import("@/components/layout/Tag"), {
ssr: false,
});
-// ๐๏ธ graphQL query
-const query = gql`
- {
- importRecords {
- nodes {
- jobId
- fileName
- submissionDate
- trackFormat {
- nickname
- }
- uploadedByUser {
- email
- }
- }
- }
- }
-`;
// ๐ฅ workaround for error when trying to pass options as props with functions
// โ "functions cannot be passed directly to client components because they're not serializable"
// ๐๏ธ used to change options for @/components/table/DataTable
const cntx = "imported";
-export default async function Page({ endpoint }: GraphqlEndPoint) {
+export default async function Page() {
+ // ๐๏ธ role base graphQL api route
+ const endpoint = await getSessionRoleEndpoint();
+
+ // ๐๏ธ graphQL query
+ const query = gql`
+ {
+ importRecords {
+ nodes {
+ jobId
+ fileName
+ submissionDate
+ trackFormat {
+ nickname
+ }
+ uploadedByUser {
+ email
+ }
+ }
+ }
+ }
+ `;
// ๐๏ธ RETURN: table with query data
return (
<>
diff --git a/app/components/routes/imported/id/Area.tsx b/app/components/routes/imported/id/Area.tsx
index 709c7af..8be115f 100644
--- a/app/components/routes/imported/id/Area.tsx
+++ b/app/components/routes/imported/id/Area.tsx
@@ -1,3 +1,4 @@
+import { getSessionRoleEndpoint } from "@/utils/postgraphile/helpers";
import { Suspense } from "react";
import { gql } from "graphql-request";
import Spinner from "@/components/common/Spinner";
@@ -13,7 +14,9 @@ const Tag = dynamic(() => import("@/components/layout/Tag"), {
});
// ๐๏ธ used to changes options for @/components/table/DataTable
const cntx = "dlpAnalysis";
-export default async function Page({ id, endpoint }: GraphqlParamEndPoint) {
+export default async function Page({ id }: GraphqlParamEndPoint) {
+ const endpoint = await getSessionRoleEndpoint();
+
// ๐๏ธ graphQL query
const query = gql`
{
diff --git a/app/i18n/client.ts b/app/i18n/client.ts
index afcb0af..eda4722 100644
--- a/app/i18n/client.ts
+++ b/app/i18n/client.ts
@@ -1,6 +1,6 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
-import { useTranslation } from "next-i18next"; //import from next-i18next instead of react-i18next prevents error "NextJS+NextI18Next hydration error when trying to map through array: "Text content does not match server-rendered HTML". This is because the response of serverSideTranslations is a custom object with _nextI18Next property.
+import { useTranslation } from "next-i18next"; //import from next-i18next instead of react-i18next prevents error "NextJS+NextI18Next hydration error when trying to map through array: "Text content does not match server-rendered HTML". This is because the response of serverSideTranslations is a custom object with _nextI18Next property.
import resourcesToBackend from "i18next-resources-to-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { getOptions } from "./settings";
diff --git a/app/i18n/locales/en/translation.json b/app/i18n/locales/en/translation.json
index e6130cf..8db1d9b 100644
--- a/app/i18n/locales/en/translation.json
+++ b/app/i18n/locales/en/translation.json
@@ -11,6 +11,9 @@
"3": "Imported By"
}
},
+ "dataset": {
+ "tag": "Anonymized dataset"
+ },
"datasets": {
"columns": {
"0": "Dataset",
@@ -66,6 +69,9 @@
"tag": "Home"
},
"imported": {
+ "dataset": {
+ "tag": "Dataset"
+ },
"datasets": {
"tag": "Anonymize a dataset"
}
@@ -192,6 +198,9 @@
},
"tag": "Imported dataset"
},
+ "dataset": {
+ "tag": "Dataset"
+ },
"datasets": {
"columns": {
"0": "Dataset",
@@ -227,7 +236,7 @@
"messages": {
"errors": {
"error": "Ops, something went wrong!",
- "label": "Attention:",
+ "label": "Error:",
"notfound": "Page cannot be found.",
"unauth": "Ops, you do not seem to have access to this area.",
"upload": {
diff --git a/app/i18n/locales/fr/translation.json b/app/i18n/locales/fr/translation.json
index 6a929c6..6e58b0c 100644
--- a/app/i18n/locales/fr/translation.json
+++ b/app/i18n/locales/fr/translation.json
@@ -236,7 +236,7 @@
}
},
"successes": {
- "label": "FR Success:",
+ "label": "Succรจs:",
"upload": "FR File uploaded."
}
}
diff --git a/app/logs/errors/file.json b/app/logs/errors/file.json
index 709dfac..6496242 100644
--- a/app/logs/errors/file.json
+++ b/app/logs/errors/file.json
@@ -12,3 +12,197 @@
{"timestamp":"2023-06-29T15:04:23.339Z","type":"upload","error":"Error: unsupported file type"}
{"timestamp":"2023-06-29T15:11:36.700Z","type":"upload","error":"unsupported"}
{"timestamp":"2023-06-29T15:13:01.317Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-05T17:14:14.966Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-05T17:20:05.277Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-17T19:33:17.347Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:33:48.083Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:34:18.816Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:34:55.469Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:35:26.255Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:35:56.784Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:40:12.523Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:40:43.196Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:41:13.711Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:41:45.857Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:42:16.689Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:42:47.191Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:47:04.994Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:47:35.627Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:48:06.154Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:48:38.454Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:53:32.643Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:54:03.402Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:54:33.892Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:55:06.121Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T19:55:36.923Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:01:38.259Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.csv&uploadType=resumable failed, reason: "}
+{"timestamp":"2023-07-17T20:02:23.288Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:02:25.197Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:02:26.951Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:04:02.140Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:15:06.876Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:20:43.615Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.json&uploadType=resumable failed, reason: "}
+{"timestamp":"2023-07-17T20:21:22.858Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:24:31.261Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-17T20:58:08.669Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.xlsx&uploadType=resumable failed, reason: "}
+{"timestamp":"2023-07-17T20:58:41.189Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-19T16:04:12.695Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:04:39.195Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:05:09.749Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:05:40.480Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:06:11.398Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:10:07.161Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-19T16:11:35.482Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:12:03.253Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-19T16:13:13.025Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-19T16:15:00.198Z","type":"upload","error":"Failed to upload file. Error: The file at echo does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/home/shon/Workspace/Button/climatetrax-frontend/echo'"}
+{"timestamp":"2023-07-19T16:15:28.636Z","type":"upload","error":"Failed to upload file. Error: The file at echo does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/home/shon/Workspace/Button/climatetrax-frontend/echo'"}
+{"timestamp":"2023-07-19T16:15:53.020Z","type":"upload","error":"Failed to upload file. Error: The file at /credentials/service-account-key-gcs.json does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/credentials'"}
+{"timestamp":"2023-07-19T16:17:45.769Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-19T16:23:54.093Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-19T16:26:13.259Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-19T16:39:40.283Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-21T13:34:07.826Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:34:37.103Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:35:08.236Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:35:39.259Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:36:10.184Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:36:40.800Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:37:11.784Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:37:42.783Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:38:13.680Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:38:44.585Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:39:15.287Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:39:46.248Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:40:17.084Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:40:48.098Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:41:18.983Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:41:50.048Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:42:20.782Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:42:51.888Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:43:22.519Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:43:53.650Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:44:24.470Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:44:55.294Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:45:26.327Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:45:57.248Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:46:28.065Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:46:58.994Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:47:29.709Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:48:00.842Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:48:31.480Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:49:02.812Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T13:49:33.166Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-21T14:00:22.334Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:00:52.995Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:01:24.171Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:01:54.685Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:03:40.481Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:04:10.387Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:04:41.509Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:05:12.324Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:05:43.562Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:06:14.245Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:06:45.099Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:07:15.828Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:07:46.881Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:08:17.583Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:08:48.698Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:09:19.419Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:09:50.342Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:10:21.394Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:10:52.287Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:11:23.116Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:11:54.093Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:12:25.191Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:12:55.889Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:13:26.582Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:13:57.851Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:14:28.590Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:14:59.482Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:16:30.835Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:22:29.751Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:22:59.953Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:23:31.084Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:24:01.801Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:24:32.683Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:25:03.550Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:25:34.374Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:26:05.399Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:26:36.120Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:27:07.187Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:27:38.332Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:28:08.831Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:28:39.936Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."}
+{"timestamp":"2023-07-21T14:32:57.516Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:33:28.340Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:33:59.237Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:34:30.341Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:35:01.240Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:35:31.971Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:36:02.803Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:36:33.898Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:37:04.794Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:37:35.447Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:48:15.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-21T14:49:56.922Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-07-24T14:36:02.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-24T14:36:32.895Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-24T14:37:58.904Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-24T14:38:29.609Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"}
+{"timestamp":"2023-07-24T14:50:12.789Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"}
+{"timestamp":"2023-07-24T14:50:43.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"}
+{"timestamp":"2023-07-24T14:51:14.148Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"}
+{"timestamp":"2023-07-24T15:01:08.578Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-08-04T14:14:19.273Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:14:49.814Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:15:20.561Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:15:51.410Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:16:22.019Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:16:52.824Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:17:23.485Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:17:54.293Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:18:24.798Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:18:55.596Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:19:26.319Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:19:56.977Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:20:27.762Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:20:58.271Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:21:29.143Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:21:59.866Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:22:30.632Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:23:01.400Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:23:32.016Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:24:02.746Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:24:33.287Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:25:04.076Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:25:34.613Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:26:05.259Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:26:36.133Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:27:06.905Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:27:37.443Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:28:08.119Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:28:39.001Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:29:09.734Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:29:40.527Z","type":"upload","error":"unsupported"}
+{"timestamp":"2023-08-04T14:31:56.370Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:32:27.051Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:32:57.966Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:33:28.444Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:33:59.138Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:34:30.087Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:35:00.549Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:35:31.296Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:36:02.165Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:36:32.916Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:37:03.643Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:37:34.348Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:38:04.849Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:38:35.465Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:39:06.384Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:39:37.146Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:40:07.844Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:40:38.347Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:41:09.160Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:42:13.000Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."}
+{"timestamp":"2023-08-04T14:46:23.694Z","type":"upload","error":"unsupported"}
diff --git a/app/styles/globals.css b/app/styles/globals.css
index 79b5905..c697251 100644
--- a/app/styles/globals.css
+++ b/app/styles/globals.css
@@ -17,4 +17,4 @@ a {
}
@tailwind base;
@tailwind components;
-@tailwind utilities;
\ No newline at end of file
+@tailwind utilities;
diff --git a/app/types/declarations.d.ts b/app/types/declarations.d.ts
index 0c81183..3e12f60 100644
--- a/app/types/declarations.d.ts
+++ b/app/types/declarations.d.ts
@@ -9,6 +9,9 @@ interface DataTableProps {
columns: any[];
cntx?: string | null;
}
+interface GraphqlEndPoint {
+ endpoint: string;
+}
interface GraphqlQuery {
endpoint: string;
@@ -23,13 +26,8 @@ interface GraphqlResponse {
};
}
-interface GraphqlEndPoint {
- endpoint: string;
-}
-
interface GraphqlParamEndPoint {
id: string;
- endpoint: string;
}
interface TagProps {
diff --git a/app/types/next-auth.d.ts b/app/types/next-auth.d.ts
index d06fcfe..d4cf8ad 100644
--- a/app/types/next-auth.d.ts
+++ b/app/types/next-auth.d.ts
@@ -12,19 +12,19 @@ declare module "next-auth" {
interface Session {
user: {
// ๐๏ธ Module augmentation to add 'role' definition to the Session object
- role?: string;
+ role?: string | any;
} & DefaultSession["user"];
}
}
declare module "next-auth" {
interface User {
// ๐๏ธ Module augmentation to add 'role' definition to the User object
- role: string;
+ role?: string | any;
}
}
declare module "next-auth/jwt" {
// ๐๏ธ Module augmentation to add 'role' definition to the JWT
interface JWT {
- role?: string;
+ role?: string | any;
}
}
diff --git a/app/utils/auth/auth.ts b/app/utils/api/auth/auth.ts
similarity index 63%
rename from app/utils/auth/auth.ts
rename to app/utils/api/auth/auth.ts
index 334f0f9..5575bcf 100644
--- a/app/utils/auth/auth.ts
+++ b/app/utils/api/auth/auth.ts
@@ -1,11 +1,11 @@
import type { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
+import GithubProvider from "next-auth/providers/github";
-//import { request, gql } from "graphql-request";
+import { request, gql } from "graphql-request";
async function getUserRole(email: string | null | undefined) {
- /*
- const endpoint = process.env.API_HOST + "api/role";
+ const endpoint = process.env.API_HOST + "api/auth/role";
const query =
gql`
{
@@ -13,15 +13,12 @@ async function getUserRole(email: string | null | undefined) {
email +
`" }) {
nodes {
- email
userrole
}
}
}`;
const data: any = await request(endpoint, query);
- return data[Object.keys(data)[0]].nodes as any[];
- */
- return "analyst"; // ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ
+ return data[Object.keys(data)[0]].nodes[0].userrole as any[];
}
export const authOptions: NextAuthOptions = {
@@ -36,17 +33,22 @@ export const authOptions: NextAuthOptions = {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
+ GithubProvider({
+ clientId: process.env.GITHUB_ID as string,
+ clientSecret: process.env.GITHUB_SECRET as string,
+ }),
],
callbacks: {
async session({ session, token }) {
- // ๐๏ธ add role to the token from our permissions table
if (!token.role) {
+ // ๐๏ธ add role to the token from our permissions table
const role = await getUserRole(token.email);
if (role) {
// ๐๏ธ OK: set JWT role from our user record
token.role = role;
}
}
+
return {
...session,
user: {
@@ -57,22 +59,14 @@ export const authOptions: NextAuthOptions = {
};
},
// ๐๏ธ called whenever a JSON Web Token is created - we can add to the JWT in this callback
- async jwt({ token, user }) {
- if (user) {
- const u = user as unknown as any;
+ async jwt({ token }) {
+ if (!token.role) {
// ๐๏ธ add role to the token from our permissions table
- if (!u.role) {
- const role = await getUserRole(token.email);
- if (role) {
- // ๐๏ธ OK: set JWT role from our user record
- token.role = role;
- }
+ const role = await getUserRole(token.email);
+ if (role) {
+ // ๐๏ธ OK: set JWT role from our user record
+ token.role = role;
}
- return {
- ...token,
- id: u.id,
- role: u.role,
- };
}
return token;
},
diff --git a/app/utils/upload/post.ts b/app/utils/api/upload/post.ts
similarity index 97%
rename from app/utils/upload/post.ts
rename to app/utils/api/upload/post.ts
index d5af9d9..d9f882a 100644
--- a/app/utils/upload/post.ts
+++ b/app/utils/api/upload/post.ts
@@ -64,18 +64,19 @@ export default async function handler(request: NextRequest) {
// ๐๏ธ check file type
const fileType = uploadedFile.type;
let isValidFileType = false;
-
+ console.log(fileType);
switch (fileType) {
case "application/json":
case "application/vnd.ms-excel":
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
+ case "application/xml":
case "text/csv":
case "text/xml":
- // ๐
+ // ๐ yes
isValidFileType = true;
break;
default:
- // ๐
+ // ๐ no
success = false;
break;
}
diff --git a/app/utils/helpers.tsx b/app/utils/helpers.tsx
index 79e0464..2f529fa 100644
--- a/app/utils/helpers.tsx
+++ b/app/utils/helpers.tsx
@@ -1,85 +1,3 @@
-import { cookies } from "next/headers";
-import { request, Variables } from "graphql-request";
-import { GraphqlResponse } from "@/types/declarations";
-import fs from "fs";
-
-/**
- * Flatten a nested JSON object
- * @param object - The JSON object with nested JSON objects
- */
-export const flattenJSON = (
- object: Record
-): Record => {
- let simpleObj: Record = {};
- for (let key in object) {
- const value = object[key];
- const type = typeof value;
- if (
- ["string", "boolean"].includes(type) ||
- (type === "number" && !isNaN(value))
- ) {
- simpleObj[key] = value;
- } else if (type === "object") {
- // Recursive loop
- Object.assign(simpleObj, flattenJSON(value));
- }
- }
- return simpleObj;
-};
-
-/**
- * Perform a graphql-request to the API endpoint
- * @param endpoint - The API route
- * @param query - The postgraphile query
- */
-export const getQueryData = async (
- endpoint: string,
- query: string
-): Promise => {
- // Variables for graphql-request
- endpoint = process.env.API_HOST + endpoint;
-
- const variables: Variables = {};
- // IMPORTANT: Add the browser session cookie with the encrypted JWT to this server-side API request
- // To be used by middleware for route protection
- const headers = {
- Cookie:
- "next-auth.session-token=" +
- cookies().get("next-auth.session-token")?.value,
- };
- // Data fetching via graphql-request
- const response: GraphqlResponse = await request(
- endpoint,
- query,
- variables,
- headers
- );
- // Get the nodes of the first object from the response
- const nodes = response[Object.keys(response)[0]].nodes;
- // Flatten nested nodes
- const data = nodes.map((obj) => flattenJSON(obj));
-
- // Return the data
- return data;
-};
-
-/**
- * Get a property by path utility - an alternative to lodash.get
- * @param object - The object to traverse
- * @param path - The path to the property
- * @param defaultValue - The default value if the property is not found
- */
-export const getPropByPath = (
- object: Record,
- path: string | string[],
- defaultValue?: any
-): any => {
- const myPath = Array.isArray(path) ? path : path.split(".");
- if (object && myPath.length)
- return getPropByPath(object[myPath.shift()!], myPath, defaultValue);
- return object === undefined ? defaultValue : object;
-};
-
/**
* Log message to stout or local json file
* @param message - The message to log
diff --git a/app/utils/insight/tools.tsx b/app/utils/insight/tools.tsx
index 8d79b52..da4b6ca 100644
--- a/app/utils/insight/tools.tsx
+++ b/app/utils/insight/tools.tsx
@@ -25,4 +25,4 @@ export const biTools = [
text: "insight.tools.looker",
tool: "looker"
},
-];
\ No newline at end of file
+];
diff --git a/app/utils/navigation/patties/manager.tsx b/app/utils/navigation/patties/manager.tsx
index f1a8024..d5e61a7 100644
--- a/app/utils/navigation/patties/manager.tsx
+++ b/app/utils/navigation/patties/manager.tsx
@@ -13,15 +13,7 @@ export const menu: MenuItem[] = [
button: "home.routes.home.button",
},
{
- href: `${baseUrl}anonymized`,
- button: "home.routes.anonymized.button",
- },
- {
- href: `${baseUrl}insight`,
+ href: `${baseUrl}test`,
button: "home.routes.insight.button",
},
- {
- href: `${baseUrl}analytic`,
- button: "home.routes.analytic.button",
- },
];
diff --git a/app/utils/navigation/routes/manager.tsx b/app/utils/navigation/routes/manager.tsx
index 4299639..3db114a 100644
--- a/app/utils/navigation/routes/manager.tsx
+++ b/app/utils/navigation/routes/manager.tsx
@@ -2,22 +2,10 @@
import { RouteItem } from "@/types/declarations";
export const routes: RouteItem[] = [
- {
- button: "home.routes.anonymized.button",
- content: "home.routes.anonymized.content",
- href: "anonymized",
- title: "home.routes.anonymized.title",
- },
{
button: "home.routes.insight.button",
content: "home.routes.insight.content",
- href: "insight",
+ href: "test",
title: "home.routes.insight.title",
},
- {
- button: "home.routes.analytic.button",
- content: "home.routes.analytic.content",
- href: "analytic",
- title: "home.routes.analytic.title",
- },
];
diff --git a/app/utils/postgraphile/QueryRunner.js b/app/utils/postgraphile/QueryRunner.js
new file mode 100644
index 0000000..92883cf
--- /dev/null
+++ b/app/utils/postgraphile/QueryRunner.js
@@ -0,0 +1,87 @@
+const { Pool } = require("pg");
+const { graphql } = require("graphql");
+const {
+ withPostGraphileContext,
+ createPostGraphileSchema,
+} = require("postgraphile");
+
+async function makeQueryRunner(
+ connectionString,
+ schemaName,
+ options // See https://www.graphile.org/postgraphile/usage-schema/ for options
+) {
+ // Create the PostGraphile schema
+ const schema = await createPostGraphileSchema(
+ connectionString,
+ schemaName,
+ options
+ );
+ // Our database pool
+ const pgPool = new Pool({
+ connectionString,
+ });
+
+ console.log(
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
+ );
+ console.log(connectionString);
+ console.log(schemaName);
+ console.log(options);
+ console.log(schema);
+ // The query function for issuing GraphQL queries
+ const query = async (
+ graphqlQuery, // e.g. `{ __typename }`
+ variables = {},
+ jwtToken = null, // A string, or null
+ operationName = null
+ ) => {
+ console.log(
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
+ );
+ console.log(schema);
+ console.log(graphqlQuery);
+ // pgSettings and additionalContextFromRequest cannot be functions at this point
+ const pgSettings = options.pgSettings;
+
+ console.log(graphqlQuery);
+ return await withPostGraphileContext(
+ {
+ ...options,
+ pgPool,
+ jwtToken: jwtToken,
+ pgSettings,
+ },
+ async (context) => {
+ console.log(
+ "============================================================================="
+ );
+ console.log(schema);
+ console.log(graphqlQuery);
+ // Do NOT use context outside of this function.
+ return await graphql(
+ schema,
+ graphqlQuery,
+ null,
+ {
+ ...context,
+ /* You can add more to context if you like */
+ },
+ variables,
+ operationName
+ );
+ }
+ );
+ };
+
+ // Should we need to release this query runner, the cleanup tasks:
+ const release = () => {
+ pgPool.end();
+ };
+
+ return {
+ query,
+ release,
+ };
+}
+
+exports.makeQueryRunner = makeQueryRunner;
diff --git a/app/utils/postgraphile/helpers.tsx b/app/utils/postgraphile/helpers.tsx
new file mode 100644
index 0000000..d327310
--- /dev/null
+++ b/app/utils/postgraphile/helpers.tsx
@@ -0,0 +1,84 @@
+import { cookies } from "next/headers";
+import { request, Variables } from "graphql-request";
+import { GraphqlResponse } from "@/types/declarations";
+import { authOptions } from "@/utils/api/auth/auth";
+import { getServerSession } from "next-auth/next";
+/**
+ * Flatten a nested JSON object
+ * @param object - The JSON object with nested JSON objects
+ */
+export const flattenJSON = (
+ object: Record
+): Record => {
+ try {
+ let simpleObj: Record = {};
+ for (let key in object) {
+ const value = object[key];
+ const type = typeof value;
+ if (
+ ["string", "boolean"].includes(type) ||
+ (type === "number" && !isNaN(value))
+ ) {
+ simpleObj[key] = value;
+ } else if (type === "object") {
+ // Recursive loop
+ Object.assign(simpleObj, flattenJSON(value));
+ }
+ }
+ return simpleObj;
+ } catch (error) {
+ console.error("An error occurred:", error);
+ throw error; // Re-throw the error to be caught by the calling code
+ }
+};
+
+/**
+ * Get server side session user role
+ */
+
+export const getSessionRoleEndpoint = async (): Promise => {
+ const session = await getServerSession(authOptions);
+ const role = session?.user?.role ?? "";
+ const endpoint = "api/" + role + "/graphql";
+ return endpoint;
+};
+
+/**
+ * Perform a graphql-request to the API endpoint
+ * @param endpoint - The API route
+ * @param query - The postgraphile query
+ */
+export const getQueryData = async (
+ endpoint: string,
+ query: string
+): Promise => {
+ try {
+ // ๐๏ธ variables for graphql-request
+ const variables: Variables = {};
+ // โ IMPORTANT - ๐ช cookies: For serverside requests, add the browser session cookie with the encrypted JWT to this server-side API request to be used by middleware for route protection
+ const headers = {
+ Cookie:
+ "next-auth.session-token=" +
+ cookies().get("next-auth.session-token")?.value,
+ };
+ // console.log(cookies().get("next-auth.session-token")?.value);
+ endpoint = process.env.API_HOST + endpoint;
+ // ๐๏ธ data fetching via graphql-request
+ const response: GraphqlResponse = await request(
+ endpoint,
+ query,
+ variables,
+ headers
+ );
+
+ // ๐ช mild hack....
+ // ๐๏ธ get the nodes of the first object from the response
+ const nodes = response[Object.keys(response)[0]].nodes;
+ // ๐๏ธ flatten nested nodes
+ const data = nodes.map((obj) => flattenJSON(obj));
+ return data;
+ } catch (error) {
+ console.error("An error occurred:", error);
+ throw error;
+ }
+};
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 8bf7b1a..9b3fc9e 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -10,4 +10,3 @@ services:
- 3000:3000
volumes:
- .:/usr/src/app
-
diff --git a/gitleaks.toml b/gitleaks.toml
deleted file mode 100644
index c97ffd1..0000000
--- a/gitleaks.toml
+++ /dev/null
@@ -1,557 +0,0 @@
-title = "gitleaks config"
-
-# Gitleaks rules are defined by regular expressions and entropy ranges.
-# Some secrets have unique signatures which make detecting those secrets easy.
-# Examples of those secrets would be Gitlab Personal Access Tokens, AWS keys, and Github Access Tokens.
-# All these examples have defined prefixes like `glpat`, `AKIA`, `ghp_`, etc.
-#
-# Other secrets might just be a hash which means we need to write more complex rules to verify
-# that what we are matching is a secret.
-#
-# Here is an example of a semi-generic secret
-#
-# discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"
-#
-# We can write a regular expression to capture the variable name (identifier),
-# the assignment symbol (like '=' or ':='), and finally the actual secret.
-# The structure of a rule to match this example secret is below:
-#
-# Beginning string
-# quotation
-# โ End string quotation
-# โ โ
-# โผ โผ
-# (?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"]
-#
-# โฒ โฒ โฒ
-# โ โ โ
-# โ โ โ
-# identifier assignment symbol
-# Secret
-#
-
-[[rules]]
-id = "gitlab-pat"
-description = "GitLab Personal Access Token"
-regex = '''glpat-[0-9a-zA-Z\-]{20}'''
-
-[[rules]]
-id = "aws-access-token"
-description = "AWS"
-regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
-
-# Cryptographic keys
-[[rules]]
-id = "PKCS8-PK"
-description = "PKCS8 private key"
-regex = '''-----BEGIN PRIVATE KEY-----'''
-
-[[rules]]
-id = "RSA-PK"
-description = "RSA private key"
-regex = '''-----BEGIN RSA PRIVATE KEY-----'''
-
-[[rules]]
-id = "OPENSSH-PK"
-description = "SSH private key"
-regex = '''-----BEGIN OPENSSH PRIVATE KEY-----'''
-
-[[rules]]
-id = "PGP-PK"
-description = "PGP private key"
-regex = '''-----BEGIN PGP PRIVATE KEY BLOCK-----'''
-
-[[rules]]
-id = "github-pat"
-description = "Github Personal Access Token"
-regex = '''ghp_[0-9a-zA-Z]{36}'''
-
-[[rules]]
-id = "github-oauth"
-description = "Github OAuth Access Token"
-regex = '''gho_[0-9a-zA-Z]{36}'''
-
-[[rules]]
-id = "SSH-DSA-PK"
-description = "SSH (DSA) private key"
-regex = '''-----BEGIN DSA PRIVATE KEY-----'''
-
-[[rules]]
-id = "SSH-EC-PK"
-description = "SSH (EC) private key"
-regex = '''-----BEGIN EC PRIVATE KEY-----'''
-
-
-[[rules]]
-id = "github-app-token"
-description = "Github App Token"
-regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}'''
-
-[[rules]]
-id = "github-refresh-token"
-description = "Github Refresh Token"
-regex = '''ghr_[0-9a-zA-Z]{76}'''
-
-[[rules]]
-id = "shopify-shared-secret"
-description = "Shopify shared secret"
-regex = '''shpss_[a-fA-F0-9]{32}'''
-
-[[rules]]
-id = "shopify-access-token"
-description = "Shopify access token"
-regex = '''shpat_[a-fA-F0-9]{32}'''
-
-[[rules]]
-id = "shopify-custom-access-token"
-description = "Shopify custom app access token"
-regex = '''shpca_[a-fA-F0-9]{32}'''
-
-[[rules]]
-id = "shopify-private-app-access-token"
-description = "Shopify private app access token"
-regex = '''shppa_[a-fA-F0-9]{32}'''
-
-[[rules]]
-id = "slack-access-token"
-description = "Slack token"
-regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
-
-[[rules]]
-id = "stripe-access-token"
-description = "Stripe"
-regex = '''(?is)(sk|pk)_(test|live)_[0-9a-z]{10,32}'''
-
-[[rules]]
-id = "pypi-upload-token"
-description = "PyPI upload token"
-regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}'''
-
-[[rules]]
-id = "gcp-service-account"
-description = "Google (GCP) Service-account"
-regex = '''\"type\": \"service_account\"'''
-
-[[rules]]
-id = "heroku-api-key"
-description = "Heroku API Key"
-regex = ''' (?is)(heroku[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "slack-web-hook"
-description = "Slack Webhook"
-regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}'''
-
-[[rules]]
-id = "twilio-api-key"
-description = "Twilio API Key"
-regex = '''SK[0-9a-fA-F]{32}'''
-
-[[rules]]
-id = "age-secret-key"
-description = "Age secret key"
-regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}'''
-
-[[rules]]
-id = "facebook-token"
-description = "Facebook token"
-regex = '''(?is)(facebook[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "twitter-token"
-description = "Twitter token"
-regex = '''(?is)(twitter[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{35,44})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "adobe-client-id"
-description = "Adobe Client ID (Oauth Web)"
-regex = '''(?is)(adobe[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "adobe-client-secret"
-description = "Adobe Client Secret"
-regex = '''(p8e-)(?is)[a-z0-9]{32}'''
-
-[[rules]]
-id = "alibaba-access-key-id"
-description = "Alibaba AccessKey ID"
-regex = '''(LTAI)(?is)[a-z0-9]{20}'''
-
-[[rules]]
-id = "alibaba-secret-key"
-description = "Alibaba Secret Key"
-regex = '''(?is)(alibaba[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "asana-client-id"
-description = "Asana Client ID"
-regex = '''(?is)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{16})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "asana-client-secret"
-description = "Asana Client Secret"
-regex = '''(?is)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "atlassian-api-token"
-description = "Atlassian API token"
-regex = '''(?is)(atlassian[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{24})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "bitbucket-client-id"
-description = "Bitbucket client ID"
-regex = '''(?is)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "bitbucket-client-secret"
-description = "Bitbucket client secret"
-regex = '''(?is)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9_\-]{64})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "beamer-api-token"
-description = "Beamer API token"
-regex = '''(?is)(beamer[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](b_[a-z0-9=_\-]{44})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "clojars-api-token"
-description = "Clojars API token"
-regex = '''(CLOJARS_)(?is)[a-z0-9]{60}'''
-
-[[rules]]
-id = "contentful-delivery-api-token"
-description = "Contentful delivery API token"
-regex = '''(?is)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "contentful-preview-api-token"
-description = "Contentful preview API token"
-regex = '''(?is)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "databricks-api-token"
-description = "Databricks API token"
-regex = '''dapi[a-h0-9]{32}'''
-
-[[rules]]
-id = "discord-api-token"
-description = "Discord API key"
-regex = '''(?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "discord-client-id"
-description = "Discord client ID"
-regex = '''(?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{18})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "discord-client-secret"
-description = "Discord client secret"
-regex = '''(?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "doppler-api-token"
-description = "Doppler API token"
-regex = '''['\"](dp\.pt\.)(?is)[a-z0-9]{43}['\"]'''
-
-[[rules]]
-id = "dropbox-api-secret"
-description = "Dropbox API secret/key"
-regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]'''
-
-[[rules]]
-id = "dropbox--api-key"
-description = "Dropbox API secret/key"
-regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]'''
-
-[[rules]]
-id = "dropbox-short-lived-api-token"
-description = "Dropbox short lived API token"
-regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](sl\.[a-z0-9\-=_]{135})['\"]'''
-
-[[rules]]
-id = "dropbox-long-lived-api-token"
-description = "Dropbox long lived API token"
-regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"][a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43}['\"]'''
-
-[[rules]]
-id = "duffel-api-token"
-description = "Duffel API token"
-regex = '''['\"]duffel_(test|live)_(?is)[a-z0-9_-]{43}['\"]'''
-
-[[rules]]
-id = "dynatrace-api-token"
-description = "Dynatrace API token"
-regex = '''['\"]dt0c01\.(?is)[a-z0-9]{24}\.[a-z0-9]{64}['\"]'''
-
-[[rules]]
-id = "easypost-api-token"
-description = "EasyPost API token"
-regex = '''['\"]EZAK(?is)[a-z0-9]{54}['\"]'''
-
-[[rules]]
-id = "easypost-test-api-token"
-description = "EasyPost test API token"
-regex = '''['\"]EZTK(?is)[a-z0-9]{54}['\"]'''
-
-[[rules]]
-id = "fastly-api-token"
-description = "Fastly API token"
-regex = '''(?is)(fastly[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "finicity-client-secret"
-description = "Finicity client secret"
-regex = '''(?is)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{20})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "finicity-api-token"
-description = "Finicity API token"
-regex = '''(?is)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "flutterweave-public-key"
-description = "Flutterweave public key"
-regex = '''FLWPUBK_TEST-(?is)[a-h0-9]{32}-X'''
-
-[[rules]]
-id = "flutterweave-secret-key"
-description = "Flutterweave secret key"
-regex = '''FLWSECK_TEST-(?is)[a-h0-9]{32}-X'''
-
-[[rules]]
-id = "flutterweave-enc-key"
-description = "Flutterweave encrypted key"
-regex = '''FLWSECK_TEST[a-h0-9]{12}'''
-
-[[rules]]
-id = "frameio-api-token"
-description = "Frame.io API token"
-regex = '''fio-u-(?is)[a-z0-9-_=]{64}'''
-
-[[rules]]
-id = "gocardless-api-token"
-description = "GoCardless API token"
-regex = '''['\"]live_(?is)[a-z0-9-_=]{40}['\"]'''
-
-[[rules]]
-id = "grafana-api-token"
-description = "Grafana API token"
-regex = '''['\"]eyJrIjoi(?is)[a-z0-9-_=]{72,92}['\"]'''
-
-[[rules]]
-id = "hashicorp-tf-api-token"
-description = "Hashicorp Terraform user/org API token"
-regex = '''['\"](?is)[a-z0-9]{14}\.atlasv1\.[a-z0-9-_=]{60,70}['\"]'''
-
-[[rules]]
-id = "hubspot-api-token"
-description = "Hubspot API token"
-regex = '''(?is)(hubspot[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "intercom-api-token"
-description = "Intercom API token"
-regex = '''(?is)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_]{60})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "intercom-client-secret"
-description = "Intercom client secret/ID"
-regex = '''(?is)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "ionic-api-token"
-description = "Ionic API token"
-regex = '''(?is)(ionic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](ion_[a-z0-9]{42})['\"]'''
-
-[[rules]]
-id = "linear-api-token"
-description = "Linear API token"
-regex = '''lin_api_(?is)[a-z0-9]{40}'''
-
-[[rules]]
-id = "linear-client-secret"
-description = "Linear client secret/ID"
-regex = '''(?is)(linear[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "lob-api-key"
-description = "Lob API Key"
-regex = '''(?is)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((live|test)_[a-f0-9]{35})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "lob-pub-api-key"
-description = "Lob Publishable API Key"
-regex = '''(?is)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((test|live)_pub_[a-f0-9]{31})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "mailchimp-api-key"
-description = "Mailchimp API key"
-regex = '''(?is)(mailchimp[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32}-us20)['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "mailgun-private-api-token"
-description = "Mailgun private API token"
-regex = '''(?is)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](key-[a-f0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "mailgun-pub-key"
-description = "Mailgun public validation key"
-regex = '''(?is)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](pubkey-[a-f0-9]{32})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "mailgun-signing-key"
-description = "Mailgun webhook signing key"
-regex = '''(?is)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "mapbox-api-token"
-description = "Mapbox API token"
-regex = '''(?is)(pk\.[a-z0-9]{60}\.[a-z0-9]{22})'''
-
-[[rules]]
-id = "messagebird-api-token"
-description = "MessageBird API token"
-regex = '''(?is)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{25})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "messagebird-client-id"
-description = "MessageBird API client ID"
-regex = '''(?is)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "new-relic-user-api-key"
-description = "New Relic user API Key"
-regex = '''['\"](NRAK-[A-Z0-9]{27})['\"]'''
-
-[[rules]]
-id = "new-relic-user-api-id"
-description = "New Relic user API ID"
-regex = '''(?is)(newrelic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([A-Z0-9]{64})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "new-relic-browser-api-token"
-description = "New Relic ingest browser API token"
-regex = '''['\"](NRJS-[a-f0-9]{19})['\"]'''
-
-[[rules]]
-id = "npm-access-token"
-description = "npm access token"
-regex = '''['\"](npm_(?is)[a-z0-9]{36})['\"]'''
-
-[[rules]]
-id = "planetscale-password"
-description = "Planetscale password"
-regex = '''pscale_pw_(?is)[a-z0-9\-_\.]{43}'''
-
-[[rules]]
-id = "planetscale-api-token"
-description = "Planetscale API token"
-regex = '''pscale_tkn_(?is)[a-z0-9\-_\.]{43}'''
-
-[[rules]]
-id = "postman-api-token"
-description = "Postman API token"
-regex = '''PMAK-(?is)[a-f0-9]{24}\-[a-f0-9]{34}'''
-
-[[rules]]
-id = "pulumi-api-token"
-description = "Pulumi API token"
-regex = '''pul-[a-f0-9]{40}'''
-
-[[rules]]
-id = "rubygems-api-token"
-description = "Rubygem API token"
-regex = '''rubygems_[a-f0-9]{48}'''
-
-[[rules]]
-id = "sendgrid-api-token"
-description = "Sendgrid API token"
-regex = '''SG\.(?is)[a-z0-9_\-\.]{66}'''
-
-[[rules]]
-id = "sendinblue-api-token"
-description = "Sendinblue API token"
-regex = '''xkeysib-[a-f0-9]{64}\-(?is)[a-z0-9]{16}'''
-
-[[rules]]
-id = "shippo-api-token"
-description = "Shippo API token"
-regex = '''shippo_(live|test)_[a-f0-9]{40}'''
-
-[[rules]]
-id = "linedin-client-secret"
-description = "Linkedin Client secret"
-regex = '''(?is)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z]{16})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "linedin-client-id"
-description = "Linkedin Client ID"
-regex = '''(?is)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{14})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "twitch-api-token"
-description = "Twitch API token"
-regex = '''(?is)(twitch[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]'''
-secretGroup = 3
-
-[[rules]]
-id = "typeform-api-token"
-description = "Typeform API token"
-regex = '''(?is)(typeform[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}(tfp_[a-z0-9\-_\.=]{59})'''
-secretGroup = 3
-
-# A generic rule to match things that look like a secret
-# (?is): i means case insensitive, m means multiline.
-# We allow for unlimited whitespace character on each side of the assignment operator
-[[rules]]
-id = "generic-api-key"
-description = "Generic API Key"
-regex = '''(?is)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,50})\s*(=|>|:=|\|\|:|<=|=>|:)\s*.{0,5}['\"]([0-9a-zA-Z\-_=]{8,192})['\"]'''
-entropy = 3.7
-secretGroup = 4
-
-[[rules]]
-description = "Database password"
-regex = '''databasePW\s*=\s*".*?"'''
-tags = ["database", "password"]
-
-[allowlist]
-description = "global allow lists"
-regexes = ['''(example_regex)''']
-paths = [
- '''.gitleaks.toml''',
- '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$'''
-]
diff --git a/middlewares/withAuthorization.tsx b/middlewares/withAuthorization.tsx
index b358547..1c3b969 100644
--- a/middlewares/withAuthorization.tsx
+++ b/middlewares/withAuthorization.tsx
@@ -19,9 +19,19 @@ export const withAuthorization: MiddlewareFactory = (next: NextMiddleware) => {
// ๐๏ธ vars for route management
const { pathname } = request.nextUrl;
+ const isRouteGraphQL = pathname.indexOf("api/postgraph") > -1;
const isRouteAuth =
pathname.indexOf("/auth") > -1 || pathname.indexOf("/unauth") > -1;
+ // ๐๏ธ check calls to graphql
+ if (isRouteGraphQL === true) {
+ // ๐ dev only
+ if (process.env.API_HOST == "http://localhost:3000/") {
+ // ๐๏ธ OK: route all postgraphile routes
+ return NextResponse.next();
+ }
+ }
+
// ๐๏ธ check if authentication route
if (isRouteAuth === true) {
const { query } = parse(request.url, true);
@@ -35,14 +45,14 @@ export const withAuthorization: MiddlewareFactory = (next: NextMiddleware) => {
// ๐๏ธ return response
NextResponse.next();
} else {
- // ๐๏ธ vars for user session details via next-auth getToken to decrypt jwt in request cookie
- const session = await getToken({
+ // ๐๏ธ vars for user token details via next-auth getToken to decrypt jwt in request cookie
+ const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
});
- const role = session?.role ? session?.role : "analyst"; // ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ ๐จ;
+ const role = token?.role;
- if (session && role) {
+ if (token && role) {
// ๐๏ธ OK: authenticated and authorized role
// ๐๏ธ validate routes properties
diff --git a/middlewares/withLocalization.tsx b/middlewares/withLocalization.tsx
index 13bb72b..696c466 100644
--- a/middlewares/withLocalization.tsx
+++ b/middlewares/withLocalization.tsx
@@ -11,6 +11,7 @@ export const withLocalization: MiddlewareFactory = (next) => {
return async (request: NextRequest, _next: NextFetchEvent) => {
// 1๏ธโฃ Check (valid) Language Prefix in URL
const { pathname } = request.nextUrl;
+
//๐๏ธ the first non-empty segment is considered the language prefix
const [languagePrefix] = pathname.split("/").filter(Boolean);
// ๐๏ธ validate the language is supported from the accepted languages
diff --git a/next.config.js b/next.config.js
index 6a3b0d9..a40865a 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,7 +1,12 @@
/** @type {import("next").NextConfig} */
+require("dotenv").config();
+
const nextConfig = {
experimental: {
appDir: true,
},
+ env: {
+ API_HOST: process.env.API_HOST,
+ },
};
module.exports = nextConfig;
diff --git a/package.json b/package.json
index 4e0dbe4..0a928d3 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,11 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "k8s": "minikube start && scripts/k8s-secrets.sh && scripts/k8s-launch.sh",
+ "test:codegen": "npx playwright codegen",
+ "test:e2e": "playwright test",
+ "test:i18n": "playwright test tests/i18n",
+ "test:gcs": "scripts/tests/test-gcs.sh",
+ "k8s": "minikube start && scripts/k8s-secrets.sh",
"tailwind:watch": "postcss tailwind.css -o styles.css -w"
},
"dependencies": {
@@ -26,6 +30,8 @@
"@types/react-dom": "18.2.4",
"accept-language": "3.0.18",
"autoprefixer": "10.4.14",
+ "bufferutil": "^4.0.7",
+ "encoding": "^0.1.13",
"eslint": "8.40.0",
"eslint-config-next": "13.4.2",
"eslint-config-prettier": "^8.8.0",
@@ -33,10 +39,12 @@
"eslint-plugin-react": "^7.32.2",
"fetch-blob": "^4.0.0",
"formdata-polyfill": "^4.0.10",
+ "graphql": "^16.7.1",
"graphql-request": "^5.2.0",
"i18next": "^22.5.1",
"i18next-browser-languagedetector": "7.0.1",
"i18next-resources-to-backend": "1.1.3",
+ "mock-express-request": "^0.2.2",
"mui-datatables": "^4.3.0",
"next": "latest",
"next-auth": "^4.22.1",
@@ -52,7 +60,8 @@
"react-i18next": "12.1.1",
"stream": "^0.0.2",
"tailwindcss": "3.3.2",
- "typescript": "5.0.4"
+ "typescript": "5.0.4",
+ "utf-8-validate": "^6.0.3"
},
"devDependencies": {
"@playwright/test": "^1.35.0",
diff --git a/pages/api/analyst/graphql.ts b/pages/api/analyst/graphql.ts
index 58936f4..ed0d092 100644
--- a/pages/api/analyst/graphql.ts
+++ b/pages/api/analyst/graphql.ts
@@ -1,3 +1,37 @@
+/*
+Next.js pages\API route handler method receives two arguments: req and res.
+
+The req argument represents the incoming HTTP request to the API route.
+It contains information about the request, such as headers, query parameters, request body, and more.
+It is an instance of the Node.js http.IncomingMessage class.
+
+The res argument represents the HTTP response object that you use to send a response back to the client.
+ It provides methods for setting response headers, writing the response body, and managing the response status code.
+ It is an instance of the Node.js http.ServerResponse class.
+
+The req object is an instance of http.IncomingMessage, and it represents the request information received from the client sending the request,
+it also includes the request body, query, headers, method, URL, etc.
+Next.js also provides built-in middlewares for parsing and extending the incoming request (req) object. These are the middlewares:
+
+req.body: By default, Next.js parses the request body, so you donโt have to install another third-party body-parser module. req.body comprises an object parsed by content-type as specified by the client. It defaults to null if no body was specified.
+
+req.query: Next.js also parses the query string attached to the request URL. req.query is an object containing the query parameters and their values, or an empty object {} if no query string was attached.
+
+req.cookies: It contains the cookies sent by the request. It also defaults to an empty object {} if no cookies are specified.
+
+The response (res) object. It is an instance of http.ServerResponse, with additional helper methods
+
+res.json(body): It is used to send a JSON response back to the client. It takes as an argument an object which must be serializable.
+
+res.send(body): Used to send the HTTP response. The body can either be a string, an object or a buffer.
+
+res.status(code): It is a function used to set the response status code. It accepts a valid HTTP status code as an argument.
+
+Next.js provides a config object that, when exported, can be used to change some of the default configurations of the application. It has a nested api object that deals with configurations available for API routes.
+
+For example, we can disable the default body parser provided by Next.js.
+*/
+
import { postgraphile } from "postgraphile";
import { pgAnalyst } from "@/utils/postgraphile/pool/pgAnalyst";
import { options } from "@/utils/postgraphile/options";
@@ -6,19 +40,27 @@ const databaseSchemaAdmin = process.env.DATABASE_SCHEMA_ADMIN || "";
const databaseSchemaClean = process.env.DATABASE_SCHEMA_CLEAN || "";
const databaseSchemaWorkspace = process.env.DATABASE_SCHEMA_WORKSPACE || "";
+// ๐๏ธ customize the default configuration of the API route by exporting a config object in the same file
+export const config = {
+ api: {
+ bodyParser: false, // Defaults to true. Setting this to false disables body parsing and allows you to consume the request body as stream or raw-body.
+ responseLimit: false, // Determines how much data should be sent from the response body. It is automatically enabled and defaults to 4mb.
+ externalResolver: true, // Disables warnings for unresolved requests if the route is being handled by an external resolver
+ },
+};
+
+// ๐๏ธ postgraphile function returns an object assigned to the requestHandler variable
const requestHandler = postgraphile(
pgAnalyst,
[databaseSchemaAdmin, databaseSchemaClean, databaseSchemaWorkspace],
{
...options,
+ // ๐๏ธ specifies the role based route where this GraphQL API will be accessible
graphqlRoute: "/api/analyst/graphql",
}
);
-export const config = {
- api: {
- bodyParser: false,
- externalResolver: true,
- },
-};
+/*When a request is made to the specified Next.js route, the requestHandler executes the logic provided by PostGraphile.
+It connects to the PostgreSQL database using the provided connection pool (pgAnalyst) and exposes a GraphQL API based on the specified schemas and options.
+It handles the execution of GraphQL queries and returns the corresponding data as per the GraphQL request.*/
export default requestHandler;
diff --git a/pages/api/auth/role.ts b/pages/api/auth/role.ts
new file mode 100644
index 0000000..9d69ff2
--- /dev/null
+++ b/pages/api/auth/role.ts
@@ -0,0 +1,20 @@
+import { postgraphile } from "postgraphile";
+import { pgAdmin } from "@/utils/postgraphile/pool/pgAdmin";
+import { options } from "@/utils/postgraphile/options";
+
+const requestHandler = postgraphile(
+ pgAdmin,
+ process.env.DATABASE_SCHEMA_ADMIN,
+ {
+ ...options,
+ graphqlRoute: "/api/auth/role",
+ }
+);
+
+export const config = {
+ api: {
+ bodyParser: false,
+ externalResolver: true,
+ },
+};
+export default requestHandler;
diff --git a/pages/api/graphiql.ts b/pages/api/postgraphiql.ts
similarity index 87%
rename from pages/api/graphiql.ts
rename to pages/api/postgraphiql.ts
index dfb2b29..4deb773 100644
--- a/pages/api/graphiql.ts
+++ b/pages/api/postgraphiql.ts
@@ -11,8 +11,8 @@ const requestHandler = postgraphile(
[databaseSchemaAdmin, databaseSchemaClean, databaseSchemaWorkspace],
{
...options,
- graphiqlRoute: "/api/graphiql",
- graphqlRoute: "/api/graphql",
+ graphiqlRoute: "/api/postgraphiql",
+ graphqlRoute: "/api/postgraphql",
}
);
diff --git a/pages/api/graphql.ts b/pages/api/postgraphql.ts
similarity index 94%
rename from pages/api/graphql.ts
rename to pages/api/postgraphql.ts
index de48789..97604cb 100644
--- a/pages/api/graphql.ts
+++ b/pages/api/postgraphql.ts
@@ -11,7 +11,7 @@ const requestHandler = postgraphile(
[databaseSchemaAdmin, databaseSchemaClean, databaseSchemaWorkspace],
{
...options,
- graphqlRoute: "/api/graphql",
+ graphqlRoute: "/api/postgraphql",
}
);
diff --git a/playwright.config.ts b/playwright.config.ts
index cdcccfe..be4d854 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,77 +1,51 @@
-import { defineConfig, devices } from '@playwright/test';
+import { PlaywrightTestConfig, devices } from "@playwright/test";
+import path from "path";
+
+// Use process.env.PORT by default and fallback to port 3000
+const PORT = process.env.PORT || 3000;
+
+// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port
+const baseURL = `http://localhost:${PORT}`;
+
+// Reference: https://playwright.dev/docs/test-configuration
+const config: PlaywrightTestConfig = {
+ // Timeout per test
+ timeout: 30 * 1000,
+ // Test directory
+ testDir: path.join(__dirname, "tests"),
+ // If a test fails, retry it additional 2 times
+ retries: 2,
+ // Artifacts folder where screenshots, videos, and traces are stored.
+ outputDir: "test-results/",
-/**
- * Read environment variables from file.
- * https://github.com/motdotla/dotenv
- */
-// require('dotenv').config();
-
-/**
- * See https://playwright.dev/docs/test-configuration.
- */
-export default defineConfig({
- testDir: './tests',
- /* Run tests in files in parallel */
- fullyParallel: true,
- /* Fail the build on CI if you accidentally left test.only in the source code. */
- forbidOnly: !!process.env.CI,
- /* Retry on CI only */
- retries: process.env.CI ? 2 : 0,
- /* Opt out of parallel tests on CI. */
- workers: process.env.CI ? 1 : undefined,
- /* Reporter to use. See https://playwright.dev/docs/test-reporters */
- reporter: 'html',
- /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
- /* Base URL to use in actions like `await page.goto('/')`. */
- // baseURL: 'http://127.0.0.1:3000',
+ // Use baseURL so to make navigations relative.
+ // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url
+ baseURL,
+
+ // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc.
+ // More information: https://playwright.dev/docs/trace-viewer
+ trace: "retry-with-trace",
- /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
- trace: 'on-first-retry',
+ // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context
+ // contextOptions: {
+ // ignoreHTTPSErrors: true,
+ // },
},
- /* Configure projects for major browsers */
projects: [
+ // Setup project
+ { name: "setup", testMatch: /.*\.setup\.ts/ },
{
- name: 'chromium',
- use: { ...devices['Desktop Chrome'] },
- },
-
- {
- name: 'firefox',
- use: { ...devices['Desktop Firefox'] },
- },
-
- {
- name: 'webkit',
- use: { ...devices['Desktop Safari'] },
+ name: "chromium",
+ use: {
+ ...devices["Desktop Chrome"],
+ // Use prepared auth state.
+ storageState: "playwright/.auth/user.json",
+ },
+ dependencies: ["setup"],
},
-
- /* Test against mobile viewports. */
- // {
- // name: 'Mobile Chrome',
- // use: { ...devices['Pixel 5'] },
- // },
- // {
- // name: 'Mobile Safari',
- // use: { ...devices['iPhone 12'] },
- // },
-
- /* Test against branded browsers. */
- // {
- // name: 'Microsoft Edge',
- // use: { ...devices['Desktop Edge'], channel: 'msedge' },
- // },
- // {
- // name: 'Google Chrome',
- // use: { ..devices['Desktop Chrome'], channel: 'chrome' },
- // },
+ // Test against mobile viewports...
],
-
- /* Run your local dev server before starting the tests */
- // webServer: {
- // command: 'npm run start',
- // url: 'http://127.0.0.1:3000',
- // reuseExistingServer: !process.env.CI,
- // },
-});
+};
+export default config;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 25fea30..421bb92 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3,7 +3,7 @@ lockfileVersion: '6.0'
dependencies:
'@google-cloud/storage':
specifier: ^6.11.0
- version: 6.11.0
+ version: 6.11.0(encoding@0.1.13)
'@graphile-contrib/pg-order-by-related':
specifier: ^1.0.0
version: 1.0.0
@@ -12,7 +12,7 @@ dependencies:
version: 6.1.0
'@graphile/pg-aggregates':
specifier: ^0.1.1
- version: 0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@15.8.0)
+ version: 0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@16.7.1)
'@headlessui/react':
specifier: ^1.7.15
version: 1.7.15(react-dom@18.2.0)(react@18.2.0)
@@ -30,7 +30,7 @@ dependencies:
version: 5.13.5(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mui/x-data-grid':
specifier: latest
- version: 6.9.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0)
+ version: 6.10.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0)
'@types/node':
specifier: 20.2.0
version: 20.2.0
@@ -46,6 +46,12 @@ dependencies:
autoprefixer:
specifier: 10.4.14
version: 10.4.14(postcss@8.4.23)
+ bufferutil:
+ specifier: ^4.0.7
+ version: 4.0.7
+ encoding:
+ specifier: ^0.1.13
+ version: 0.1.13
eslint:
specifier: 8.40.0
version: 8.40.0
@@ -67,9 +73,12 @@ dependencies:
formdata-polyfill:
specifier: ^4.0.10
version: 4.0.10
+ graphql:
+ specifier: ^16.7.1
+ version: 16.7.1
graphql-request:
specifier: ^5.2.0
- version: 5.2.0(graphql@15.8.0)
+ version: 5.2.0(encoding@0.1.13)(graphql@16.7.1)
i18next:
specifier: ^22.5.1
version: 22.5.1
@@ -79,21 +88,24 @@ dependencies:
i18next-resources-to-backend:
specifier: 1.1.3
version: 1.1.3
+ mock-express-request:
+ specifier: ^0.2.2
+ version: 0.2.2
mui-datatables:
specifier: ^4.3.0
version: 4.3.0(@emotion/react@11.11.1)(@mui/icons-material@5.11.16)(@mui/material@5.13.5)(react-dom@18.2.0)(react@18.2.0)
next:
specifier: latest
- version: 13.4.7(react-dom@18.2.0)(react@18.2.0)
+ version: 13.4.10(react-dom@18.2.0)(react@18.2.0)
next-auth:
specifier: ^4.22.1
- version: 4.22.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0)
+ version: 4.22.1(next@13.4.10)(react-dom@18.2.0)(react@18.2.0)
next-i18next:
specifier: ^13.3.0
- version: 13.3.0(i18next@22.5.1)(next@13.4.7)(react-i18next@12.1.1)(react@18.2.0)
+ version: 13.3.0(i18next@22.5.1)(next@13.4.10)(react-i18next@12.1.1)(react@18.2.0)
next-runtime-dotenv:
specifier: ^1.5.1
- version: 1.5.1(next@13.4.7)
+ version: 1.5.1(next@13.4.10)
pg:
specifier: ^8.11.0
version: 8.11.0
@@ -102,7 +114,7 @@ dependencies:
version: 8.4.23
postgraphile:
specifier: ^4.13.0
- version: 4.13.0
+ version: 4.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
postgraphile-plugin-connection-filter:
specifier: ^2.3.0
version: 2.3.0
@@ -127,6 +139,9 @@ dependencies:
typescript:
specifier: 5.0.4
version: 5.0.4
+ utf-8-validate:
+ specifier: ^6.0.3
+ version: 6.0.3
devDependencies:
'@playwright/test':
@@ -401,7 +416,7 @@ packages:
engines: {node: '>=12'}
dev: false
- /@google-cloud/storage@6.11.0:
+ /@google-cloud/storage@6.11.0(encoding@0.1.13):
resolution: {integrity: sha512-p5VX5K2zLTrMXlKdS1CiQNkKpygyn7CBFm5ZvfhVj6+7QUsjWvYx9YDMkYXdarZ6JDt4cxiu451y9QUIH82ZTw==}
engines: {node: '>=12'}
dependencies:
@@ -414,13 +429,13 @@ packages:
duplexify: 4.1.2
ent: 2.2.0
extend: 3.0.2
- gaxios: 5.1.2
- google-auth-library: 8.8.0
+ gaxios: 5.1.2(encoding@0.1.13)
+ google-auth-library: 8.8.0(encoding@0.1.13)
mime: 3.0.0
mime-types: 2.1.35
p-limit: 3.1.0
retry-request: 5.0.2
- teeny-request: 8.0.3
+ teeny-request: 8.0.3(encoding@0.1.13)
uuid: 8.3.2
transitivePeerDependencies:
- encoding
@@ -442,7 +457,7 @@ packages:
tslib: 2.5.3
dev: false
- /@graphile/pg-aggregates@0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@15.8.0):
+ /@graphile/pg-aggregates@0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@16.7.1):
resolution: {integrity: sha512-bPfniRw4oN9nNP8tkRlbBslNMA38fhVWNhhaReODhPVEshwquzUmSmSCtSVhS4J+StEFgrP7Z+z1IN0/ror2XA==}
peerDependencies:
graphile-build: ^4.12.0-alpha.0
@@ -452,20 +467,20 @@ packages:
'@types/debug': 4.1.8
'@types/graphql': 14.5.0
debug: 4.3.4
- graphile-build: 4.13.0(graphql@15.8.0)
- graphile-build-pg: 4.13.0(graphql@15.8.0)(pg@8.11.0)
+ graphile-build: 4.13.0(graphql@16.7.1)
+ graphile-build-pg: 4.13.0(graphql@16.7.1)(pg@8.11.0)
graphile-utils: 4.13.0(graphile-build-pg@4.13.0)(graphile-build@4.13.0)
- graphql: 15.8.0
+ graphql: 16.7.1
transitivePeerDependencies:
- supports-color
dev: false
- /@graphql-typed-document-node/core@3.2.0(graphql@15.8.0):
+ /@graphql-typed-document-node/core@3.2.0(graphql@16.7.1):
resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
dependencies:
- graphql: 15.8.0
+ graphql: 16.7.1
dev: false
/@headlessui/react@1.7.15(react-dom@18.2.0)(react@18.2.0):
@@ -630,7 +645,7 @@ packages:
'@babel/runtime': 7.22.5
'@emotion/is-prop-valid': 1.2.1
'@mui/types': 7.2.4(@types/react@18.2.6)
- '@mui/utils': 5.13.1(react@18.2.0)
+ '@mui/utils': 5.13.7(react@18.2.0)
'@popperjs/core': 2.11.8
'@types/react': 18.2.6
clsx: 1.2.1
@@ -705,7 +720,7 @@ packages:
optional: true
dependencies:
'@babel/runtime': 7.22.5
- '@mui/utils': 5.13.1(react@18.2.0)
+ '@mui/utils': 5.13.7(react@18.2.0)
'@types/react': 18.2.6
prop-types: 15.8.1
react: 18.2.0
@@ -753,7 +768,7 @@ packages:
'@mui/private-theming': 5.13.1(@types/react@18.2.6)(react@18.2.0)
'@mui/styled-engine': 5.13.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0)
'@mui/types': 7.2.4(@types/react@18.2.6)
- '@mui/utils': 5.13.1(react@18.2.0)
+ '@mui/utils': 5.13.7(react@18.2.0)
'@types/react': 18.2.6
clsx: 1.2.1
csstype: 3.1.2
@@ -783,8 +798,21 @@ packages:
react: 18.2.0
react-is: 18.2.0
- /@mui/x-data-grid@6.9.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-JlvLvYHUrfFBQMQa7fzkxUmEVygQL5IWt0cww1Rs4zKYaVcDM5v1T8ZhFOrOQp04c7meB+CykuP94r3Dn30wqQ==}
+ /@mui/utils@5.13.7(react@18.2.0):
+ resolution: {integrity: sha512-/3BLptG/q0u36eYED7Nhf4fKXmcKb6LjjT7ZMwhZIZSdSxVqDqSTmATW3a56n3KEPQUXCU9TpxAfCBQhs6brVA==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ react: ^17.0.0 || ^18.0.0
+ dependencies:
+ '@babel/runtime': 7.22.5
+ '@types/prop-types': 15.7.5
+ '@types/react-is': 18.2.1
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-is: 18.2.0
+
+ /@mui/x-data-grid@6.10.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-x9h+Z4B2vu+ZKKwClBVs30Y9eZYdhqyV3toHH2E0zat7FIZxwiVfk6qz4Q98V1fV0Fe1nczPj9i0siUmduMEXg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@mui/material': ^5.4.1
@@ -795,7 +823,7 @@ packages:
'@babel/runtime': 7.22.5
'@mui/material': 5.13.5(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mui/system': 5.13.5(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
- '@mui/utils': 5.13.1(react@18.2.0)
+ '@mui/utils': 5.13.7(react@18.2.0)
clsx: 1.2.1
prop-types: 15.8.1
react: 18.2.0
@@ -803,8 +831,8 @@ packages:
reselect: 4.1.8
dev: false
- /@next/env@13.4.7:
- resolution: {integrity: sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==}
+ /@next/env@13.4.10:
+ resolution: {integrity: sha512-3G1yD/XKTSLdihyDSa8JEsaWOELY+OWe08o0LUYzfuHp1zHDA8SObQlzKt+v+wrkkPcnPweoLH1ImZeUa0A1NQ==}
dev: false
/@next/eslint-plugin-next@13.4.2:
@@ -813,8 +841,8 @@ packages:
glob: 7.1.7
dev: false
- /@next/swc-darwin-arm64@13.4.7:
- resolution: {integrity: sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==}
+ /@next/swc-darwin-arm64@13.4.10:
+ resolution: {integrity: sha512-4bsdfKmmg7mgFGph0UorD1xWfZ5jZEw4kKRHYEeTK9bT1QnMbPVPlVXQRIiFPrhoDQnZUoa6duuPUJIEGLV1Jg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -822,8 +850,8 @@ packages:
dev: false
optional: true
- /@next/swc-darwin-x64@13.4.7:
- resolution: {integrity: sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==}
+ /@next/swc-darwin-x64@13.4.10:
+ resolution: {integrity: sha512-ngXhUBbcZIWZWqNbQSNxQrB9T1V+wgfCzAor2olYuo/YpaL6mUYNUEgeBMhr8qwV0ARSgKaOp35lRvB7EmCRBg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -831,8 +859,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-arm64-gnu@13.4.7:
- resolution: {integrity: sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==}
+ /@next/swc-linux-arm64-gnu@13.4.10:
+ resolution: {integrity: sha512-SjCZZCOmHD4uyM75MVArSAmF5Y+IJSGroPRj2v9/jnBT36SYFTORN8Ag/lhw81W9EeexKY/CUg2e9mdebZOwsg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -840,8 +868,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-arm64-musl@13.4.7:
- resolution: {integrity: sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==}
+ /@next/swc-linux-arm64-musl@13.4.10:
+ resolution: {integrity: sha512-F+VlcWijX5qteoYIOxNiBbNE8ruaWuRlcYyIRK10CugqI/BIeCDzEDyrHIHY8AWwbkTwe6GRHabMdE688Rqq4Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -849,8 +877,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-x64-gnu@13.4.7:
- resolution: {integrity: sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==}
+ /@next/swc-linux-x64-gnu@13.4.10:
+ resolution: {integrity: sha512-WDv1YtAV07nhfy3i1visr5p/tjiH6CeXp4wX78lzP1jI07t4PnHHG1WEDFOduXh3WT4hG6yN82EQBQHDi7hBrQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -858,8 +886,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-x64-musl@13.4.7:
- resolution: {integrity: sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==}
+ /@next/swc-linux-x64-musl@13.4.10:
+ resolution: {integrity: sha512-zFkzqc737xr6qoBgDa3AwC7jPQzGLjDlkNmt/ljvQJ/Veri5ECdHjZCUuiTUfVjshNIIpki6FuP0RaQYK9iCRg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -867,8 +895,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-arm64-msvc@13.4.7:
- resolution: {integrity: sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==}
+ /@next/swc-win32-arm64-msvc@13.4.10:
+ resolution: {integrity: sha512-IboRS8IWz5mWfnjAdCekkl8s0B7ijpWeDwK2O8CdgZkoCDY0ZQHBSGiJ2KViAG6+BJVfLvcP+a2fh6cdyBr9QQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -876,8 +904,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-ia32-msvc@13.4.7:
- resolution: {integrity: sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==}
+ /@next/swc-win32-ia32-msvc@13.4.10:
+ resolution: {integrity: sha512-bSA+4j8jY4EEiwD/M2bol4uVEu1lBlgsGdvM+mmBm/BbqofNBfaZ2qwSbwE2OwbAmzNdVJRFRXQZ0dkjopTRaQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -885,8 +913,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-x64-msvc@13.4.7:
- resolution: {integrity: sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==}
+ /@next/swc-win32-x64-msvc@13.4.10:
+ resolution: {integrity: sha512-g2+tU63yTWmcVQKDGY0MV1PjjqgZtwM4rB1oVVi/v0brdZAcrcTV+04agKzWtvWroyFz6IqtT0MoZJA7PNyLVw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -982,7 +1010,7 @@ packages:
resolution: {integrity: sha512-MOkzsEp1Jk5bXuAsHsUi6BVv0zCO+7/2PTiZMXWDSsMXvNU6w/PLMQT2vHn8hy2i0JqojPz1Sz6rsFjHtsU0lA==}
deprecated: This is a stub types definition. graphql provides its own type definitions, so you do not need this installed.
dependencies:
- graphql: 16.6.0
+ graphql: 16.7.1
dev: false
/@types/hoist-non-react-statics@3.3.1:
@@ -1147,6 +1175,14 @@ packages:
stable: 0.1.8
dev: false
+ /accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+ dev: false
+
/acorn-jsx@5.3.2(acorn@8.8.2):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1434,6 +1470,14 @@ packages:
engines: {node: '>=4'}
dev: false
+ /bufferutil@4.0.7:
+ resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==}
+ engines: {node: '>=6.14.2'}
+ requiresBuild: true
+ dependencies:
+ node-gyp-build: 4.6.0
+ dev: false
+
/bundle-name@3.0.0:
resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==}
engines: {node: '>=12'}
@@ -1595,10 +1639,10 @@ packages:
path-type: 4.0.0
yaml: 1.10.2
- /cross-fetch@3.1.6:
+ /cross-fetch@3.1.6(encoding@0.1.13):
resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==}
dependencies:
- node-fetch: 2.6.11
+ node-fetch: 2.6.11(encoding@0.1.13)
transitivePeerDependencies:
- encoding
dev: false
@@ -1808,6 +1852,12 @@ packages:
engines: {node: '>= 0.8'}
dev: false
+ /encoding@0.1.13:
+ resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
+ dependencies:
+ iconv-lite: 0.6.3
+ dev: false
+
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
@@ -2394,6 +2444,11 @@ packages:
tslib: 2.5.3
dev: false
+ /fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: false
@@ -2422,24 +2477,24 @@ packages:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: false
- /gaxios@5.1.2:
+ /gaxios@5.1.2(encoding@0.1.13):
resolution: {integrity: sha512-mPyw3qQq6qoHWTe27CrzhSj7XYKVStTGrpP92a91FfogBWOd9BMW8GT5yS5WhEYGw02AgB1fVQVSAO+JKiQP0w==}
engines: {node: '>=12'}
dependencies:
extend: 3.0.2
https-proxy-agent: 5.0.1
is-stream: 2.0.1
- node-fetch: 2.6.11
+ node-fetch: 2.6.11(encoding@0.1.13)
transitivePeerDependencies:
- encoding
- supports-color
dev: false
- /gcp-metadata@5.2.0:
+ /gcp-metadata@5.2.0(encoding@0.1.13):
resolution: {integrity: sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==}
engines: {node: '>=12'}
dependencies:
- gaxios: 5.1.2
+ gaxios: 5.1.2(encoding@0.1.13)
json-bigint: 1.0.0
transitivePeerDependencies:
- encoding
@@ -2562,7 +2617,7 @@ packages:
slash: 4.0.0
dev: false
- /google-auth-library@8.8.0:
+ /google-auth-library@8.8.0(encoding@0.1.13):
resolution: {integrity: sha512-0iJn7IDqObDG5Tu9Tn2WemmJ31ksEa96IyK0J0OZCpTh6CrC6FrattwKX87h3qKVuprCJpdOGKc1Xi8V0kMh8Q==}
engines: {node: '>=12'}
dependencies:
@@ -2570,9 +2625,9 @@ packages:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
fast-text-encoding: 1.0.6
- gaxios: 5.1.2
- gcp-metadata: 5.2.0
- gtoken: 6.1.2
+ gaxios: 5.1.2(encoding@0.1.13)
+ gcp-metadata: 5.2.0(encoding@0.1.13)
+ gtoken: 6.1.2(encoding@0.1.13)
jws: 4.0.0
lru-cache: 6.0.0
transitivePeerDependencies:
@@ -2612,7 +2667,27 @@ packages:
chalk: 2.4.2
debug: 4.3.4
graphile-build: 4.13.0(graphql@15.8.0)
- jsonwebtoken: 9.0.0
+ jsonwebtoken: 9.0.1
+ lodash: 4.17.21
+ lru-cache: 4.1.5
+ pg: 8.11.0
+ pg-sql2: 4.13.0(pg@8.11.0)
+ transitivePeerDependencies:
+ - graphql
+ - supports-color
+ dev: false
+
+ /graphile-build-pg@4.13.0(graphql@16.7.1)(pg@8.11.0):
+ resolution: {integrity: sha512-1FD+3wjCdK1lbICY1QVO26A7s8efSjR522LarL9Bx1M1iBJHNIpCEW2PK+LkulQjY1l5LGQ1A93GQFqi6cZ6bg==}
+ engines: {node: '>=8.6'}
+ peerDependencies:
+ pg: '>=6.1.0 <9'
+ dependencies:
+ '@graphile/lru': 4.11.0
+ chalk: 2.4.2
+ debug: 4.3.4
+ graphile-build: 4.13.0(graphql@16.7.1)
+ jsonwebtoken: 9.0.1
lodash: 4.17.21
lru-cache: 4.1.5
pg: 8.11.0
@@ -2642,6 +2717,26 @@ packages:
- supports-color
dev: false
+ /graphile-build@4.13.0(graphql@16.7.1):
+ resolution: {integrity: sha512-KPBrHgRw5fury6l9WEQH6ys1UtnxrRrG+Ehnr68NvfNELp4T+QsekTSVFi5LWoJOaXvdYMqP2L8MFBRQP2vKsw==}
+ engines: {node: '>=8.6'}
+ peerDependencies:
+ graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0'
+ dependencies:
+ '@graphile/lru': 4.11.0
+ chalk: 2.4.2
+ debug: 4.3.4
+ graphql: 16.7.1
+ graphql-parse-resolve-info: 4.13.0(graphql@16.7.1)
+ iterall: 1.3.0
+ lodash: 4.17.21
+ lru-cache: 5.1.1
+ pluralize: 7.0.0
+ semver: 6.3.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/graphile-utils@4.13.0(graphile-build-pg@4.13.0)(graphile-build@4.13.0):
resolution: {integrity: sha512-6nzlCNeJB1qV9AaPyJ/iHU+CDfs8jxpcmQ47Fmrgmp8r5VwKdL/uDt0LW8IuXu2VZrbM1GGyZ8rQtcdVmQYZ+g==}
engines: {node: '>=8.6'}
@@ -2650,8 +2745,8 @@ packages:
graphile-build-pg: ^4.5.0
dependencies:
debug: 4.3.4
- graphile-build: 4.13.0(graphql@15.8.0)
- graphile-build-pg: 4.13.0(graphql@15.8.0)(pg@8.11.0)
+ graphile-build: 4.13.0(graphql@16.7.1)
+ graphile-build-pg: 4.13.0(graphql@16.7.1)(pg@8.11.0)
graphql: 15.8.0
tslib: 2.5.3
transitivePeerDependencies:
@@ -2671,16 +2766,29 @@ packages:
- supports-color
dev: false
- /graphql-request@5.2.0(graphql@15.8.0):
+ /graphql-parse-resolve-info@4.13.0(graphql@16.7.1):
+ resolution: {integrity: sha512-VVJ1DdHYcR7hwOGQKNH+QTzuNgsLA8l/y436HtP9YHoX6nmwXRWq3xWthU3autMysXdm0fQUbhTZCx0W9ICozw==}
+ engines: {node: '>=8.6'}
+ peerDependencies:
+ graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0 || ^16.3.0'
+ dependencies:
+ debug: 4.3.4
+ graphql: 16.7.1
+ tslib: 2.5.3
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /graphql-request@5.2.0(encoding@0.1.13)(graphql@16.7.1):
resolution: {integrity: sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ==}
peerDependencies:
graphql: 14 - 16
dependencies:
- '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0)
- cross-fetch: 3.1.6
+ '@graphql-typed-document-node/core': 3.2.0(graphql@16.7.1)
+ cross-fetch: 3.1.6(encoding@0.1.13)
extract-files: 9.0.0
form-data: 3.0.1
- graphql: 15.8.0
+ graphql: 16.7.1
transitivePeerDependencies:
- encoding
dev: false
@@ -2699,16 +2807,16 @@ packages:
engines: {node: '>= 10.x'}
dev: false
- /graphql@16.6.0:
- resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==}
+ /graphql@16.7.1:
+ resolution: {integrity: sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
dev: false
- /gtoken@6.1.2:
+ /gtoken@6.1.2(encoding@0.1.13):
resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==}
engines: {node: '>=12.0.0'}
dependencies:
- gaxios: 5.1.2
+ gaxios: 5.1.2(encoding@0.1.13)
google-p12-pem: 4.0.1
jws: 4.0.0
transitivePeerDependencies:
@@ -2855,6 +2963,13 @@ packages:
safer-buffer: 2.1.2
dev: false
+ /iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ safer-buffer: 2.1.2
+ dev: false
+
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
@@ -3118,8 +3233,8 @@ packages:
hasBin: true
dev: false
- /jsonwebtoken@9.0.0:
- resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==}
+ /jsonwebtoken@9.0.1:
+ resolution: {integrity: sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==}
engines: {node: '>=12', npm: '>=6'}
dependencies:
jws: 3.2.2
@@ -3333,6 +3448,22 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: false
+ /mock-express-request@0.2.2:
+ resolution: {integrity: sha512-EymHjY1k1jWIsaVaCsPdFterWO18gcNwQMb99OryhSBtIA33SZJujOLeOe03Rf2DTV997xLPyl2I098WCFm/mA==}
+ dependencies:
+ accepts: 1.3.8
+ fresh: 0.5.2
+ lodash: 4.17.21
+ mock-req: 0.2.0
+ parseurl: 1.3.3
+ range-parser: 1.2.1
+ type-is: 1.6.18
+ dev: false
+
+ /mock-req@0.2.0:
+ resolution: {integrity: sha512-IUuwS0W5GjoPyjhuXPQJXpaHfHW7UYFRia8Cchm/xRuyDDclpSQdEoakt3krOpSYvgVlQsbnf0ePDsTRDfp7Dg==}
+ dev: false
+
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: false
@@ -3399,7 +3530,12 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: false
- /next-auth@4.22.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0):
+ /negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
+ /next-auth@4.22.1(next@13.4.10)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==}
peerDependencies:
next: ^12.2.5 || ^13
@@ -3414,7 +3550,7 @@ packages:
'@panva/hkdf': 1.1.1
cookie: 0.5.0
jose: 4.14.4
- next: 13.4.7(react-dom@18.2.0)(react@18.2.0)
+ next: 13.4.10(react-dom@18.2.0)(react@18.2.0)
oauth: 0.9.15
openid-client: 5.4.2
preact: 10.15.1
@@ -3424,7 +3560,7 @@ packages:
uuid: 8.3.2
dev: false
- /next-i18next@13.3.0(i18next@22.5.1)(next@13.4.7)(react-i18next@12.1.1)(react@18.2.0):
+ /next-i18next@13.3.0(i18next@22.5.1)(next@13.4.10)(react-i18next@12.1.1)(react@18.2.0):
resolution: {integrity: sha512-X4kgi51BCOoGdKbv87eZ8OU7ICQDg5IP+T5fNjqDY3os9ea0OKTY4YpAiVFiwcI9XimcUmSPbKO4a9jFUyYSgg==}
engines: {node: '>=14'}
peerDependencies:
@@ -3439,22 +3575,22 @@ packages:
hoist-non-react-statics: 3.3.2
i18next: 22.5.1
i18next-fs-backend: 2.1.5
- next: 13.4.7(react-dom@18.2.0)(react@18.2.0)
+ next: 13.4.10(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-i18next: 12.1.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
dev: false
- /next-runtime-dotenv@1.5.1(next@13.4.7):
+ /next-runtime-dotenv@1.5.1(next@13.4.10):
resolution: {integrity: sha512-G1NWW06geegqev1U3E90lfYYMV+xvVIwyQv2KbQCRp03jSdUbRANcEm/QQnNoJqEGQUXoEgYDdSV6jB0yv/xAQ==}
peerDependencies:
next: '>= 5.1.0'
dependencies:
dotenv: 16.1.4
- next: 13.4.7(react-dom@18.2.0)(react@18.2.0)
+ next: 13.4.10(react-dom@18.2.0)(react@18.2.0)
dev: false
- /next@13.4.7(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==}
+ /next@13.4.10(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-4ep6aKxVTQ7rkUW2fBLhpBr/5oceCuf4KmlUpvG/aXuDTIf9mexNSpabUD6RWPspu6wiJJvozZREhXhueYO36A==}
engines: {node: '>=16.8.0'}
hasBin: true
peerDependencies:
@@ -3471,7 +3607,7 @@ packages:
sass:
optional: true
dependencies:
- '@next/env': 13.4.7
+ '@next/env': 13.4.10
'@swc/helpers': 0.5.1
busboy: 1.6.0
caniuse-lite: 1.0.30001503
@@ -3482,15 +3618,15 @@ packages:
watchpack: 2.4.0
zod: 3.21.4
optionalDependencies:
- '@next/swc-darwin-arm64': 13.4.7
- '@next/swc-darwin-x64': 13.4.7
- '@next/swc-linux-arm64-gnu': 13.4.7
- '@next/swc-linux-arm64-musl': 13.4.7
- '@next/swc-linux-x64-gnu': 13.4.7
- '@next/swc-linux-x64-musl': 13.4.7
- '@next/swc-win32-arm64-msvc': 13.4.7
- '@next/swc-win32-ia32-msvc': 13.4.7
- '@next/swc-win32-x64-msvc': 13.4.7
+ '@next/swc-darwin-arm64': 13.4.10
+ '@next/swc-darwin-x64': 13.4.10
+ '@next/swc-linux-arm64-gnu': 13.4.10
+ '@next/swc-linux-arm64-musl': 13.4.10
+ '@next/swc-linux-x64-gnu': 13.4.10
+ '@next/swc-linux-x64-musl': 13.4.10
+ '@next/swc-win32-arm64-msvc': 13.4.10
+ '@next/swc-win32-ia32-msvc': 13.4.10
+ '@next/swc-win32-x64-msvc': 13.4.10
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -3501,7 +3637,7 @@ packages:
engines: {node: '>=10.5.0'}
dev: false
- /node-fetch@2.6.11:
+ /node-fetch@2.6.11(encoding@0.1.13):
resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
@@ -3510,6 +3646,7 @@ packages:
encoding:
optional: true
dependencies:
+ encoding: 0.1.13
whatwg-url: 5.0.0
dev: false
@@ -3518,6 +3655,11 @@ packages:
engines: {node: '>= 6.13.0'}
dev: false
+ /node-gyp-build@4.6.0:
+ resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
+ hasBin: true
+ dev: false
+
/node-releases@2.0.12:
resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
dev: false
@@ -3985,7 +4127,7 @@ packages:
tslib: 2.5.3
dev: false
- /postgraphile@4.13.0:
+ /postgraphile@4.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-p2VqUnsECd1XrucylK1iosvKEn96J8CWeMVWzxF7b6G21jmaETvFe2CO2q4+dKY5DFCVEF2O9pEfmUfYCKl5+A==}
engines: {node: '>=8.6'}
hasBin: true
@@ -4008,15 +4150,15 @@ packages:
http-errors: 1.8.1
iterall: 1.3.0
json5: 2.2.3
- jsonwebtoken: 9.0.0
+ jsonwebtoken: 9.0.1
parseurl: 1.3.3
pg: 8.11.0
pg-connection-string: 2.6.0
pg-sql2: 4.13.0(pg@8.11.0)
postgraphile-core: 4.13.0(graphql@15.8.0)(pg@8.11.0)
- subscriptions-transport-ws: 0.9.19(graphql@15.8.0)
+ subscriptions-transport-ws: 0.9.19(bufferutil@4.0.7)(graphql@15.8.0)(utf-8-validate@6.0.3)
tslib: 2.5.3
- ws: 7.5.9
+ ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@6.0.3)
transitivePeerDependencies:
- bufferutil
- pg-native
@@ -4128,6 +4270,11 @@ packages:
performance-now: 2.1.0
dev: false
+ /range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
/raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
@@ -4614,7 +4761,7 @@ packages:
/stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
- /subscriptions-transport-ws@0.9.19(graphql@15.8.0):
+ /subscriptions-transport-ws@0.9.19(bufferutil@4.0.7)(graphql@15.8.0)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw==}
deprecated: The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md
peerDependencies:
@@ -4625,7 +4772,7 @@ packages:
graphql: 15.8.0
iterall: 1.3.0
symbol-observable: 1.2.0
- ws: 7.5.9
+ ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@6.0.3)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@@ -4720,13 +4867,13 @@ packages:
engines: {node: '>=6'}
dev: false
- /teeny-request@8.0.3:
+ /teeny-request@8.0.3(encoding@0.1.13):
resolution: {integrity: sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==}
engines: {node: '>=12'}
dependencies:
http-proxy-agent: 5.0.0
https-proxy-agent: 5.0.1
- node-fetch: 2.6.11
+ node-fetch: 2.6.11(encoding@0.1.13)
stream-events: 1.0.5
uuid: 9.0.0
transitivePeerDependencies:
@@ -4894,6 +5041,14 @@ packages:
punycode: 2.3.0
dev: false
+ /utf-8-validate@6.0.3:
+ resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==}
+ engines: {node: '>=6.14.2'}
+ requiresBuild: true
+ dependencies:
+ node-gyp-build: 4.6.0
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
@@ -4976,7 +5131,7 @@ packages:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: false
- /ws@7.5.9:
+ /ws@7.5.9(bufferutil@4.0.7)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
engines: {node: '>=8.3.0'}
peerDependencies:
@@ -4987,6 +5142,9 @@ packages:
optional: true
utf-8-validate:
optional: true
+ dependencies:
+ bufferutil: 4.0.7
+ utf-8-validate: 6.0.3
dev: false
/xtend@4.0.2:
diff --git a/public/next.svg b/public/next.svg
index 5174b28..5bb00d4 100644
--- a/public/next.svg
+++ b/public/next.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/public/thirteen.svg b/public/thirteen.svg
index 8977c1b..db65b53 100644
--- a/public/thirteen.svg
+++ b/public/thirteen.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/public/vercel.svg b/public/vercel.svg
index d2f8422..1aeda7d 100644
--- a/public/vercel.svg
+++ b/public/vercel.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/scripts/tests/.env.example b/scripts/tests/.env.example
new file mode 100644
index 0000000..4fcdf2a
--- /dev/null
+++ b/scripts/tests/.env.example
@@ -0,0 +1 @@
+SERVICE_ACCOUNT_JSON=
\ No newline at end of file
diff --git a/scripts/tests/test-gcs.sh b/scripts/tests/test-gcs.sh
new file mode 100755
index 0000000..528f9a0
--- /dev/null
+++ b/scripts/tests/test-gcs.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Get the absolute path to the script's directory
+DIR=$(dirname "$0")
+
+# Load the variables from .env file
+. "$DIR/.env"
+
+# Create a temporary file for the service account JSON
+TEMP_FILE=$(mktemp)
+
+# Write the service account JSON to the temporary file
+echo "$SERVICE_ACCOUNT_JSON" > "$TEMP_FILE"
+
+# Set the path to the temporary service account JSON file and export it
+export GOOGLE_APPLICATION_CREDENTIALS="$TEMP_FILE"
+
+# Build the Next.js application
+npm run build
+
+# Start the Next.js application in production mode
+# For production mode, ensure a root .env file exists with the NEXTAUTH_SECRET to prevent error: [next-auth][error][NO_SECRET] https://next-auth.js.org/errors#no_secret Please define a `secret` in production. MissingSecret [MissingSecretError]: Please define a `secret` in production.
+npm run start &
+
+# Wait for a few seconds to ensure the Next.js application is ready
+sleep 5
+
+# Run your Playwright test
+npx playwright test tests/gcs
+
+# Clean up the temporary files and folder
+rm -rf "$TEMP_DIR"
+
+# Find the process ID of the Next.js application and stop it
+NEXT_JS_PID=$(ps aux | grep "npm run start" | grep -v grep | awk '{print $2}')
+kill "$NEXT_JS_PID"
diff --git a/tailwind.config.js b/tailwind.config.js
index 8e59cb7..47d3b2c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -11,4 +11,4 @@ module.exports = {
},
},
plugins: [require("@headlessui/tailwindcss")],
-};
\ No newline at end of file
+};
diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts
deleted file mode 100644
index 2fd6016..0000000
--- a/tests-examples/demo-todo-app.spec.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-import { test, expect, type Page } from '@playwright/test';
-
-test.beforeEach(async ({ page }) => {
- await page.goto('https://demo.playwright.dev/todomvc');
-});
-
-const TODO_ITEMS = [
- 'buy some cheese',
- 'feed the cat',
- 'book a doctors appointment'
-];
-
-test.describe('New Todo', () => {
- test('should allow me to add todo items', async ({ page }) => {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- // Create 1st todo.
- await newTodo.fill(TODO_ITEMS[0]);
- await newTodo.press('Enter');
-
- // Make sure the list only has one todo item.
- await expect(page.getByTestId('todo-title')).toHaveText([
- TODO_ITEMS[0]
- ]);
-
- // Create 2nd todo.
- await newTodo.fill(TODO_ITEMS[1]);
- await newTodo.press('Enter');
-
- // Make sure the list now has two todo items.
- await expect(page.getByTestId('todo-title')).toHaveText([
- TODO_ITEMS[0],
- TODO_ITEMS[1]
- ]);
-
- await checkNumberOfTodosInLocalStorage(page, 2);
- });
-
- test('should clear text input field when an item is added', async ({ page }) => {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- // Create one todo item.
- await newTodo.fill(TODO_ITEMS[0]);
- await newTodo.press('Enter');
-
- // Check that input is empty.
- await expect(newTodo).toBeEmpty();
- await checkNumberOfTodosInLocalStorage(page, 1);
- });
-
- test('should append new items to the bottom of the list', async ({ page }) => {
- // Create 3 items.
- await createDefaultTodos(page);
-
- // create a todo count locator
- const todoCount = page.getByTestId('todo-count')
-
- // Check test using different methods.
- await expect(page.getByText('3 items left')).toBeVisible();
- await expect(todoCount).toHaveText('3 items left');
- await expect(todoCount).toContainText('3');
- await expect(todoCount).toHaveText(/3/);
-
- // Check all items in one call.
- await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
- await checkNumberOfTodosInLocalStorage(page, 3);
- });
-});
-
-test.describe('Mark all as completed', () => {
- test.beforeEach(async ({ page }) => {
- await createDefaultTodos(page);
- await checkNumberOfTodosInLocalStorage(page, 3);
- });
-
- test.afterEach(async ({ page }) => {
- await checkNumberOfTodosInLocalStorage(page, 3);
- });
-
- test('should allow me to mark all items as completed', async ({ page }) => {
- // Complete all todos.
- await page.getByLabel('Mark all as complete').check();
-
- // Ensure all todos have 'completed' class.
- await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
- await checkNumberOfCompletedTodosInLocalStorage(page, 3);
- });
-
- test('should allow me to clear the complete state of all items', async ({ page }) => {
- const toggleAll = page.getByLabel('Mark all as complete');
- // Check and then immediately uncheck.
- await toggleAll.check();
- await toggleAll.uncheck();
-
- // Should be no completed classes.
- await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
- });
-
- test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
- const toggleAll = page.getByLabel('Mark all as complete');
- await toggleAll.check();
- await expect(toggleAll).toBeChecked();
- await checkNumberOfCompletedTodosInLocalStorage(page, 3);
-
- // Uncheck first todo.
- const firstTodo = page.getByTestId('todo-item').nth(0);
- await firstTodo.getByRole('checkbox').uncheck();
-
- // Reuse toggleAll locator and make sure its not checked.
- await expect(toggleAll).not.toBeChecked();
-
- await firstTodo.getByRole('checkbox').check();
- await checkNumberOfCompletedTodosInLocalStorage(page, 3);
-
- // Assert the toggle all is checked again.
- await expect(toggleAll).toBeChecked();
- });
-});
-
-test.describe('Item', () => {
-
- test('should allow me to mark items as complete', async ({ page }) => {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- // Create two items.
- for (const item of TODO_ITEMS.slice(0, 2)) {
- await newTodo.fill(item);
- await newTodo.press('Enter');
- }
-
- // Check first item.
- const firstTodo = page.getByTestId('todo-item').nth(0);
- await firstTodo.getByRole('checkbox').check();
- await expect(firstTodo).toHaveClass('completed');
-
- // Check second item.
- const secondTodo = page.getByTestId('todo-item').nth(1);
- await expect(secondTodo).not.toHaveClass('completed');
- await secondTodo.getByRole('checkbox').check();
-
- // Assert completed class.
- await expect(firstTodo).toHaveClass('completed');
- await expect(secondTodo).toHaveClass('completed');
- });
-
- test('should allow me to un-mark items as complete', async ({ page }) => {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- // Create two items.
- for (const item of TODO_ITEMS.slice(0, 2)) {
- await newTodo.fill(item);
- await newTodo.press('Enter');
- }
-
- const firstTodo = page.getByTestId('todo-item').nth(0);
- const secondTodo = page.getByTestId('todo-item').nth(1);
- const firstTodoCheckbox = firstTodo.getByRole('checkbox');
-
- await firstTodoCheckbox.check();
- await expect(firstTodo).toHaveClass('completed');
- await expect(secondTodo).not.toHaveClass('completed');
- await checkNumberOfCompletedTodosInLocalStorage(page, 1);
-
- await firstTodoCheckbox.uncheck();
- await expect(firstTodo).not.toHaveClass('completed');
- await expect(secondTodo).not.toHaveClass('completed');
- await checkNumberOfCompletedTodosInLocalStorage(page, 0);
- });
-
- test('should allow me to edit an item', async ({ page }) => {
- await createDefaultTodos(page);
-
- const todoItems = page.getByTestId('todo-item');
- const secondTodo = todoItems.nth(1);
- await secondTodo.dblclick();
- await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
- await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
- await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
-
- // Explicitly assert the new text value.
- await expect(todoItems).toHaveText([
- TODO_ITEMS[0],
- 'buy some sausages',
- TODO_ITEMS[2]
- ]);
- await checkTodosInLocalStorage(page, 'buy some sausages');
- });
-});
-
-test.describe('Editing', () => {
- test.beforeEach(async ({ page }) => {
- await createDefaultTodos(page);
- await checkNumberOfTodosInLocalStorage(page, 3);
- });
-
- test('should hide other controls when editing', async ({ page }) => {
- const todoItem = page.getByTestId('todo-item').nth(1);
- await todoItem.dblclick();
- await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
- await expect(todoItem.locator('label', {
- hasText: TODO_ITEMS[1],
- })).not.toBeVisible();
- await checkNumberOfTodosInLocalStorage(page, 3);
- });
-
- test('should save edits on blur', async ({ page }) => {
- const todoItems = page.getByTestId('todo-item');
- await todoItems.nth(1).dblclick();
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
-
- await expect(todoItems).toHaveText([
- TODO_ITEMS[0],
- 'buy some sausages',
- TODO_ITEMS[2],
- ]);
- await checkTodosInLocalStorage(page, 'buy some sausages');
- });
-
- test('should trim entered text', async ({ page }) => {
- const todoItems = page.getByTestId('todo-item');
- await todoItems.nth(1).dblclick();
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
-
- await expect(todoItems).toHaveText([
- TODO_ITEMS[0],
- 'buy some sausages',
- TODO_ITEMS[2],
- ]);
- await checkTodosInLocalStorage(page, 'buy some sausages');
- });
-
- test('should remove the item if an empty text string was entered', async ({ page }) => {
- const todoItems = page.getByTestId('todo-item');
- await todoItems.nth(1).dblclick();
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
-
- await expect(todoItems).toHaveText([
- TODO_ITEMS[0],
- TODO_ITEMS[2],
- ]);
- });
-
- test('should cancel edits on escape', async ({ page }) => {
- const todoItems = page.getByTestId('todo-item');
- await todoItems.nth(1).dblclick();
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
- await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
- await expect(todoItems).toHaveText(TODO_ITEMS);
- });
-});
-
-test.describe('Counter', () => {
- test('should display the current number of todo items', async ({ page }) => {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- // create a todo count locator
- const todoCount = page.getByTestId('todo-count')
-
- await newTodo.fill(TODO_ITEMS[0]);
- await newTodo.press('Enter');
-
- await expect(todoCount).toContainText('1');
-
- await newTodo.fill(TODO_ITEMS[1]);
- await newTodo.press('Enter');
- await expect(todoCount).toContainText('2');
-
- await checkNumberOfTodosInLocalStorage(page, 2);
- });
-});
-
-test.describe('Clear completed button', () => {
- test.beforeEach(async ({ page }) => {
- await createDefaultTodos(page);
- });
-
- test('should display the correct text', async ({ page }) => {
- await page.locator('.todo-list li .toggle').first().check();
- await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
- });
-
- test('should remove completed items when clicked', async ({ page }) => {
- const todoItems = page.getByTestId('todo-item');
- await todoItems.nth(1).getByRole('checkbox').check();
- await page.getByRole('button', { name: 'Clear completed' }).click();
- await expect(todoItems).toHaveCount(2);
- await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
- });
-
- test('should be hidden when there are no items that are completed', async ({ page }) => {
- await page.locator('.todo-list li .toggle').first().check();
- await page.getByRole('button', { name: 'Clear completed' }).click();
- await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
- });
-});
-
-test.describe('Persistence', () => {
- test('should persist its data', async ({ page }) => {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- for (const item of TODO_ITEMS.slice(0, 2)) {
- await newTodo.fill(item);
- await newTodo.press('Enter');
- }
-
- const todoItems = page.getByTestId('todo-item');
- const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
- await firstTodoCheck.check();
- await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
- await expect(firstTodoCheck).toBeChecked();
- await expect(todoItems).toHaveClass(['completed', '']);
-
- // Ensure there is 1 completed item.
- await checkNumberOfCompletedTodosInLocalStorage(page, 1);
-
- // Now reload.
- await page.reload();
- await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
- await expect(firstTodoCheck).toBeChecked();
- await expect(todoItems).toHaveClass(['completed', '']);
- });
-});
-
-test.describe('Routing', () => {
- test.beforeEach(async ({ page }) => {
- await createDefaultTodos(page);
- // make sure the app had a chance to save updated todos in storage
- // before navigating to a new view, otherwise the items can get lost :(
- // in some frameworks like Durandal
- await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
- });
-
- test('should allow me to display active items', async ({ page }) => {
- const todoItem = page.getByTestId('todo-item');
- await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
-
- await checkNumberOfCompletedTodosInLocalStorage(page, 1);
- await page.getByRole('link', { name: 'Active' }).click();
- await expect(todoItem).toHaveCount(2);
- await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
- });
-
- test('should respect the back button', async ({ page }) => {
- const todoItem = page.getByTestId('todo-item');
- await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
-
- await checkNumberOfCompletedTodosInLocalStorage(page, 1);
-
- await test.step('Showing all items', async () => {
- await page.getByRole('link', { name: 'All' }).click();
- await expect(todoItem).toHaveCount(3);
- });
-
- await test.step('Showing active items', async () => {
- await page.getByRole('link', { name: 'Active' }).click();
- });
-
- await test.step('Showing completed items', async () => {
- await page.getByRole('link', { name: 'Completed' }).click();
- });
-
- await expect(todoItem).toHaveCount(1);
- await page.goBack();
- await expect(todoItem).toHaveCount(2);
- await page.goBack();
- await expect(todoItem).toHaveCount(3);
- });
-
- test('should allow me to display completed items', async ({ page }) => {
- await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
- await checkNumberOfCompletedTodosInLocalStorage(page, 1);
- await page.getByRole('link', { name: 'Completed' }).click();
- await expect(page.getByTestId('todo-item')).toHaveCount(1);
- });
-
- test('should allow me to display all items', async ({ page }) => {
- await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
- await checkNumberOfCompletedTodosInLocalStorage(page, 1);
- await page.getByRole('link', { name: 'Active' }).click();
- await page.getByRole('link', { name: 'Completed' }).click();
- await page.getByRole('link', { name: 'All' }).click();
- await expect(page.getByTestId('todo-item')).toHaveCount(3);
- });
-
- test('should highlight the currently applied filter', async ({ page }) => {
- await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
-
- //create locators for active and completed links
- const activeLink = page.getByRole('link', { name: 'Active' });
- const completedLink = page.getByRole('link', { name: 'Completed' });
- await activeLink.click();
-
- // Page change - active items.
- await expect(activeLink).toHaveClass('selected');
- await completedLink.click();
-
- // Page change - completed items.
- await expect(completedLink).toHaveClass('selected');
- });
-});
-
-async function createDefaultTodos(page: Page) {
- // create a new todo locator
- const newTodo = page.getByPlaceholder('What needs to be done?');
-
- for (const item of TODO_ITEMS) {
- await newTodo.fill(item);
- await newTodo.press('Enter');
- }
-}
-
-async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
- return await page.waitForFunction(e => {
- return JSON.parse(localStorage['react-todos']).length === e;
- }, expected);
-}
-
-async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
- return await page.waitForFunction(e => {
- return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
- }, expected);
-}
-
-async function checkTodosInLocalStorage(page: Page, title: string) {
- return await page.waitForFunction(t => {
- return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
- }, title);
-}
diff --git a/tests/.env.example b/tests/.env.example
new file mode 100644
index 0000000..3139868
--- /dev/null
+++ b/tests/.env.example
@@ -0,0 +1,4 @@
+API_HOST=
+GOOGLE_BUCKET_NAME=
+NEXTAUTH_JWT_ANALYST=
+NEXTAUTH_SECRET=
diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts
new file mode 100644
index 0000000..5180065
--- /dev/null
+++ b/tests/auth.setup.ts
@@ -0,0 +1,70 @@
+import { test as setup, chromium } from "@playwright/test";
+const dotenv = require("dotenv");
+dotenv.config({
+ path: "./tests/.env",
+});
+
+const siteUrl = process.env.API_HOST as string;
+const authFile = "playwright/.auth/user.json";
+// ๐๏ธ hard-coded JWT obtained from app/utils/postgraphile/helpers.tsx-possibly explore jsonwebtoken to encrypt a JSON mock response
+const jwt = process.env.NEXTAUTH_JWT_ANALYST as string;
+
+// ๐๏ธ setup function defines a test case that will run before all other test cases within the test
+setup("authenticate", async () => {
+ // ๐๏ธ set up a Playwright browser instance:
+ const browser = await chromium.launch();
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ // ๐๏ธ navigate to the page where the session token needs to be mocked
+ await page.goto(`${siteUrl}`);
+
+ // ๐๏ธ add token as next-auth cookie
+ await context.addCookies([
+ {
+ name: "next-auth.session-token",
+ value: jwt,
+ domain: "localhost",
+ path: "/",
+ httpOnly: true,
+ sameSite: "Lax",
+ expires: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
+ },
+ ]);
+
+ // ๐๏ธ store the browser's signed state in the specified file path
+ // โ this allows you to persist the authentication state between tests or test runs
+ await context.storageState({ path: authFile });
+ await browser.close();
+});
+
+/*
+ // ๐๏ธ Google/MS authentication...
+ await page.locator('button[data-myprovider="Google"]').click();
+ // click redirects page to Google auth form
+ await page.fill('input[type="email"]', email);
+ await page.getByRole("button", { name: "Next" }).click();
+ // with @button.is email, click redirects page to MicroSoft auth form
+ await page.fill('input[type="email"]', email);
+ await page.getByRole("button", { name: "Next" }).click();
+ await page.locator("#i0118").fill(password);
+ await page.getByRole("button", { name: "Sign in" }).click();
+ await page.getByRole("button", { name: "Yes" }).click();
+ // โ Navigation failed...
+ await page.waitForURL(`${siteUrl}/en/analyst/home`);
+
+
+
+ // ๐๏ธ mock JSON user session...
+ const mockSession = {
+ user: {
+ id: "user-id",
+ name: name,
+ email: email,
+ role: role,
+ },
+ };
+
+ // Encrypt the mock response
+ const jwt = jwt.sign(mockResponse, secret);
+*/
diff --git a/tests/gcs/file-upload.spec.ts b/tests/gcs/file-upload.spec.ts
new file mode 100644
index 0000000..6dca4c9
--- /dev/null
+++ b/tests/gcs/file-upload.spec.ts
@@ -0,0 +1,183 @@
+//record test: npx playwright codegen http://localhost:3000/en/analyst/dataset/add
+//run tests: pnpm run test:gcs
+
+//AC: https://www.notion.so/buttoninc/Playwright-Tests-for-GSC-ba1f819ac78d4d1ea0913664330a4f1c?pvs=4
+
+import { test, expect, chromium, Page, BrowserContext } from "@playwright/test";
+import path from "path";
+import { Storage } from "@google-cloud/storage";
+
+const dotenv = require("dotenv");
+dotenv.config({
+ path: "./tests/.env",
+});
+
+// ๐๏ธ Test parameters
+const siteUrl: string | undefined = process.env.API_HOST;
+const fileNames = [
+ "helloWorld.json",
+ "helloWorld.xml",
+ "helloWorld.csv",
+ "helloWorld.xls",
+ "helloWorld.xlsx",
+];
+const filePaths = fileNames.map((fileName) =>
+ path.resolve(process.cwd(), "tests", "gcs", "files", fileName)
+);
+
+const fileFailPath = path.resolve(
+ process.cwd(),
+ "tests",
+ "gcs",
+ "files",
+ "helloWorld.ods"
+);
+
+const lngs = ["en", "fr"];
+const successMessageDictionary: { [key: string]: string } = {
+ en: "Success",
+ fr: "Succรจs",
+};
+
+// Create a new instance of the Storage class
+const storage = new Storage();
+const bucketName = process.env.GOOGLE_BUCKET_NAME as string;
+
+// ๐๏ธ Test fixtures
+interface TestFixtures {
+ browser: any;
+ context: BrowserContext;
+ page: Page;
+}
+
+// ๐๏ธ Common assertions
+async function assertIsMaskedDivHidden(page: Page) {
+ await page.waitForSelector("div.--is-masked", { state: "hidden" });
+ const isMaskedDiv = await page.$("div.--is-masked");
+ const isMaskedDivVisible = isMaskedDiv
+ ? await isMaskedDiv.isVisible()
+ : false;
+ expect(isMaskedDivVisible).toBe(false);
+}
+
+// ๐๏ธ Test case: File Upload to GCS bucket
+test.describe("File Upload to GCS bucket", () => {
+ test.beforeAll(async () => {
+ return {
+ browser: await chromium.launch(),
+ };
+ });
+
+ test.afterAll(async ({ browser }) => {
+ await browser.close();
+ });
+
+ test.beforeEach(async ({ browser }) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ return {
+ context,
+ page,
+ };
+ });
+
+ test.afterEach(async ({ context }) => {
+ await context.close();
+ });
+
+ // ๐๏ธ Loop over language paths and file paths
+ for (const lng of lngs) {
+ for (const filePath of filePaths) {
+ // ๐๏ธ Test case: File Upload - supported file
+ test(`Add dataset: file input event - onchange success (${lng}) - ${path.basename(
+ filePath
+ )}`, async ({ page }: TestFixtures) => {
+ await page.goto(`${siteUrl}/${lng}/analyst/dataset/add`);
+
+ const divElement = await page.waitForSelector("div[data-myFileInput]");
+
+ const fileChooserPromise = new Promise((resolve) => {
+ page.on("filechooser", async (fileChooser) => {
+ await fileChooser.setFiles(filePath);
+ resolve();
+ });
+ });
+
+ await divElement.click();
+ await fileChooserPromise;
+
+ // ๐๏ธ Assert the success message text
+ await page.waitForSelector(".bg-green-100");
+ const successMessageSelector = ".bg-green-100 p:nth-child(1)";
+ const successMessage = await page.textContent(successMessageSelector);
+ const expectedSuccessMessage = successMessageDictionary[lng];
+ expect(successMessage).toContain(expectedSuccessMessage);
+
+ // ๐๏ธ Assert the window mask is hidden
+ await assertIsMaskedDivHidden(page);
+
+ // ๐๏ธ Assert the file exists in the GCP Storage bucket
+ const fileName = path.basename(filePath);
+ const [fileExists] = await storage
+ .bucket(bucketName)
+ .file(fileName)
+ .exists();
+ expect(fileExists).toBe(true);
+ });
+ }
+ }
+ // ๐๏ธ Test case: File Upload - Failing file
+ test("Add dataset: file input event - failing file", async ({
+ page,
+ }: TestFixtures) => {
+ await page.goto(`${siteUrl}/en/analyst/dataset/add`);
+
+ const divElement = await page.waitForSelector("div[data-myFileInput]");
+
+ const fileChooserPromise = new Promise((resolve) => {
+ page.on("filechooser", async (fileChooser) => {
+ await fileChooser.setFiles(fileFailPath);
+ resolve();
+ });
+ });
+
+ await divElement.click();
+ await fileChooserPromise;
+
+ // ๐๏ธ Assert the error message text
+ await page.waitForSelector(".bg-orange-100");
+ const errorMessageSelector = ".bg-orange-100 p:nth-child(1)";
+ const errorMessage = await page.textContent(errorMessageSelector);
+ expect(errorMessage).toContain("Error");
+
+ // ๐๏ธ Assert the window mask is hidden
+ await assertIsMaskedDivHidden(page);
+ });
+
+ // ๐๏ธ Test case: File Upload - Cancel
+ test("Add dataset: file input event - cancel", async ({
+ page,
+ }: TestFixtures) => {
+ await page.goto(`${siteUrl}/en/analyst/dataset/add`);
+
+ const divElement = await page.waitForSelector("div[data-myFileInput]");
+
+ const fileChooserPromise = new Promise((resolve) => {
+ page.on("filechooser", async (fileChooser) => {
+ await page.evaluate(() => {
+ document.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "Escape" })
+ );
+ });
+ resolve();
+ });
+ });
+
+ await divElement.click();
+ await fileChooserPromise;
+
+ // ๐๏ธ Assert the window mask is hidden
+ await assertIsMaskedDivHidden(page);
+ });
+});
diff --git a/tests/gcs/files/helloWorld.csv b/tests/gcs/files/helloWorld.csv
new file mode 100644
index 0000000..590f07a
--- /dev/null
+++ b/tests/gcs/files/helloWorld.csv
@@ -0,0 +1,6 @@
+HelloField,WorldField,IntField
+HelloValue1,WorldValue1,1
+HelloValue1,WorldValue2,1
+HelloValue2,WorldValue2,2
+HelloValue2,WorldValue3,2
+HelloValue3,WorldValue4,2
diff --git a/tests/gcs/files/helloWorld.json b/tests/gcs/files/helloWorld.json
new file mode 100644
index 0000000..053bba0
--- /dev/null
+++ b/tests/gcs/files/helloWorld.json
@@ -0,0 +1,15 @@
+{
+ "memberHelloWorld": "Hello World",
+ "memberArray": ["Hello", "World", "Array"],
+ "memberValue": {
+ "memberString": "Hello World String",
+ "memberNumber": 1,
+ "memberArray": ["Hello", "World", "Array", "in", "Object"],
+ "memberTrue": true,
+ "memberFalse": false,
+ "memberNull": null,
+ "memberObject": {
+ "member": "Hello World as member of an object in an object"
+ }
+ }
+}
diff --git a/tests/gcs/files/helloWorld.ods b/tests/gcs/files/helloWorld.ods
new file mode 100644
index 0000000..f454b2a
Binary files /dev/null and b/tests/gcs/files/helloWorld.ods differ
diff --git a/tests/gcs/files/helloWorld.xls b/tests/gcs/files/helloWorld.xls
new file mode 100644
index 0000000..2aca8c0
Binary files /dev/null and b/tests/gcs/files/helloWorld.xls differ
diff --git a/tests/gcs/files/helloWorld.xlsx b/tests/gcs/files/helloWorld.xlsx
new file mode 100644
index 0000000..0c12530
Binary files /dev/null and b/tests/gcs/files/helloWorld.xlsx differ
diff --git a/tests/gcs/files/helloWorld.xml b/tests/gcs/files/helloWorld.xml
new file mode 100644
index 0000000..e9e2f4d
--- /dev/null
+++ b/tests/gcs/files/helloWorld.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/i18n/lng-from-cookie.spec.ts b/tests/i18n/lng-from-cookie.spec.ts
index 91586a5..270c378 100644
--- a/tests/i18n/lng-from-cookie.spec.ts
+++ b/tests/i18n/lng-from-cookie.spec.ts
@@ -134,4 +134,4 @@ test.describe("Server Language Response Tests", () => {
await expect(page).toHaveURL(/.*en-GB/);
await browser.close();
});
-});
\ No newline at end of file
+});
diff --git a/tests/i18n/lng-from-default.spec.ts b/tests/i18n/lng-from-default.spec.ts
index 732e67d..30c99d6 100644
--- a/tests/i18n/lng-from-default.spec.ts
+++ b/tests/i18n/lng-from-default.spec.ts
@@ -57,4 +57,4 @@ test.describe("Default Language Redirection", () => {
await page.goto(`${siteUrl}/fr-CA`);
await expect(page).toHaveURL(`${siteUrl}/fr-CA`);
});
-});
\ No newline at end of file
+});
diff --git a/tests/i18n/lng-from-url-prefix.spec.ts b/tests/i18n/lng-from-url-prefix.spec.ts
index 74ce8b5..a1ef0e5 100644
--- a/tests/i18n/lng-from-url-prefix.spec.ts
+++ b/tests/i18n/lng-from-url-prefix.spec.ts
@@ -1,4 +1,11 @@
-import { test, expect, chromium, Browser, BrowserContext, Page } from "@playwright/test";
+import {
+ test,
+ expect,
+ chromium,
+ Browser,
+ BrowserContext,
+ Page,
+} from "@playwright/test";
import { fallbackLng } from "../../app/i18n/settings";
import {
EN_WELCOME_MSG,
@@ -21,13 +28,15 @@ test.describe("URL Prefix Language Redirection", () => {
});
test("should redirect to the i18next default language when Accept-Language is not available or unsupported", async () => {
- const { page } = await createPageWithAcceptLanguage('unsupported-locale');
+ const { page } = await createPageWithAcceptLanguage("unsupported-locale");
await page.goto(siteUrl);
expect(page.url()).toContain(`${siteUrl}/${fallbackLng}`);
});
for (const url of enUrls) {
- test(`should contain the correct message for English at ${url}`, async ({ page }) => {
+ test(`should contain the correct message for English at ${url}`, async ({
+ page,
+ }) => {
await page.goto(url);
const pageContent = await page.textContent("body");
expect(pageContent).toContain(EN_WELCOME_MSG);
@@ -35,7 +44,9 @@ test.describe("URL Prefix Language Redirection", () => {
}
for (const url of frUrls) {
- test(`should contain the correct message for French at ${url}`, async ({ page }) => {
+ test(`should contain the correct message for French at ${url}`, async ({
+ page,
+ }) => {
await page.goto(url);
const pageContent = await page.textContent("body");
expect(pageContent).toContain(FR_WELCOME_MSG);
@@ -43,14 +54,14 @@ test.describe("URL Prefix Language Redirection", () => {
}
test(`Unsupported language prefix defaults to Accept-Language header value`, async () => {
- const { page } = await createPageWithAcceptLanguage('fr-CA');
+ const { page } = await createPageWithAcceptLanguage("fr-CA");
await page.goto(`${siteUrl}/unsupported-lng/`);
await expect(page).toHaveURL(/.*fr-CA/);
});
test(`Unsupported language prefix defaults to i18next default language`, async () => {
- const { page } = await createPageWithAcceptLanguage('unsupported-locale');
+ const { page } = await createPageWithAcceptLanguage("unsupported-locale");
await page.goto(`${siteUrl}/unsupported-lng/`);
expect(page.url()).toContain(`${siteUrl}/${fallbackLng}`);
});
-});
\ No newline at end of file
+});
diff --git a/tests/i18n/testUtils.ts b/tests/i18n/testUtils.ts
index c93139f..54d4404 100644
--- a/tests/i18n/testUtils.ts
+++ b/tests/i18n/testUtils.ts
@@ -9,13 +9,13 @@ export const enLngs = ['en', 'en-CA', 'en-GB', 'en-US', ];
export const frLngs = ['fr', 'fr-CA'];
export const lngs = enLngs.concat(frLngs);
-export const enUrls = [
+export const enUrls = [
siteUrl + "/en/",
siteUrl + "/en-CA/",
siteUrl + "/en-GB/",
siteUrl + "/en-US/"
]
-export const frUrls = [
+export const frUrls = [
siteUrl + "/fr/",
siteUrl + "/fr-CA/"
]
@@ -26,5 +26,3 @@ export async function createPageWithAcceptLanguage(locale: string = ''): Promise
const page = await context.newPage();
return { browser, context, page };
}
-
-