diff --git a/.dockerignore b/.dockerignore index e226817..50ffe0e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,42 +2,50 @@ .env .env.local Dockerfile -.git +.git .gitignore -docker-compose* - +docker-compose\* # files form git-ignore + # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies + /.pnp .pnp.js # testing + /coverage # next.js + /.next/ /out/ # production + /build /dist # misc + .DS_Store -*.pem +\*.pem # debug + npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # vercel + .vercel # typescript -*.tsbuildinfo + +\*.tsbuildinfo next-env.d.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..157d7e3 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ + +API_HOST= +DATABASE= +DATABASE_HOST= +DATABASE_PORT= +DATABASE_PROTOCOL= +DATABASE_SCHEMA_ADMIN= +DATABASE_SCHEMA_CLEAN= +DATABASE_SCHEMA_WORKSPACE= +DATABASE_SCHEMA_VAULT= +DATABASE_USER_ADMIN= +DATABASE_USER_PW_ADMIN= +DATABASE_USER_ANALYST= +DATABASE_USER_PW_ANALYST= +DATABASE_USER_DROPPER= +DATABASE_USER_PW_DROPPER= +DATABASE_USER_MANAGER= +DATABASE_USER_PW_MANAGER= +GOOGLE_BUCKET_NAME= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +NEXTAUTH_URL= +NEXTAUTH_SECRET= \ No newline at end of file diff --git a/.github/workflows/scan-code.yml b/.github/workflows/scan-code.yml index dd132bf..b1f663f 100644 --- a/.github/workflows/scan-code.yml +++ b/.github/workflows/scan-code.yml @@ -30,5 +30,15 @@ jobs: target-url: 'http://localhost:3000' package-manager: 'pnpm' - - \ No newline at end of file + call-workflow-gitleaks-scan: + uses: button-inc/gh-actions/.github/workflows/scan-code-gitleaks.yml@v0.0.1 + with: + notify-user-list: "@shon-button,@YaokunLin" + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + gitleaks-license: ${{ secrets.GITLEAKS_LICENSE}} + + call-workflow-owasp-zap-scan: + uses: button-inc/gh-actions/.github/workflows/scan-code-owasp-zap.yml@v0.0.1 + with: + target-url: "http://localhost:3000" diff --git a/.gitignore b/.gitignore index 5c51df1..a45f8f6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,11 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# local env files -.env*.local -.env.* +# all env files except .env.example +**/.env +**/.env.development +!**/.env.example + # vercel .vercel @@ -43,3 +45,7 @@ next-env.d.ts # keycloak keycloak-sa-credential.json emissions-elt-demo-ecc9c7e27bf4.json + +# playwright +/tests/.env +playwright/.auth/user.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa19795..d08ab67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,4 +56,4 @@ repos: hooks: - id: commitlint stages: [commit-msg] - additional_dependencies: ["@commitlint/config-conventional"] \ No newline at end of file + additional_dependencies: ["@commitlint/config-conventional"] diff --git a/.vscode/settings.json b/.vscode/settings.json index ed2ca4d..61a5903 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "typescript.tsdk": "node_modules\\.pnpm\\typescript@4.9.4\\node_modules\\typescript\\lib", "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file +} diff --git a/README.md b/README.md index 5555f45..5d90816 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,11 @@ See [NextAuth.js repo](https://github.com/nextauthjs/next-auth) to learn more. Within ClimateTrax, the next-auth functionality lies within folder `app\api\[...nextauth]\route.ts` and is managed within `middleware.ts` and `middlewares\withAuthorization.ts` -## `GOOGLE_APPLICATION_CREDENTIALS` +## postgraphile + +WIP + +## Authenticating with GCP The `GOOGLE_APPLICATION_CREDENTIALS` environment variable is used by various Google Cloud client libraries and command-line tools to authenticate and authorize access to Google Cloud services. It specifies the path to the service account key file, also known as the Application Default Credentials (ADC) file. @@ -208,10 +212,10 @@ To set the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable within Visu 1. Set the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: -Linux/Mac: +Linux/Mac: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json ``` -export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json +export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json ``` To echo the value of the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: @@ -248,6 +252,72 @@ On Linux or macOS: 5. Save the file. 6. Restart your terminal or run the `source` command to reload the environment variables in your current session. +## Testing + +### Playwright + +[Playwright](https://playwright.dev/) is a powerful browser automation library that allows you to control web browsers programmatically. +Playwright comes with the ability to generate tests out of the box and is a great way to quickly get started with testing. + +**Creating tests** + +You can run codegen and perform actions in the browser recorded as test scripts. Codegen will open two windows, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests, copy the tests, clear your tests as well as change the language of your tests. Playwright will generate the code for the user interactions. Codegen will look at the rendered page and figure out the recommended locator, prioritizing role, text and test id locators. If the generator identifies multiple elements matching the locator, it will improve the locator to make it resilient and uniquely identify the target element, therefore eliminating and reducing test(s) failing and flaking due to locators. + +Use the codegen command to run the test generator followed by the URL of the website you want to generate tests for. The URL is optional and you can always run the command without it and then add the URL directly into the browser window instead. + +``` +npx playwright codegen http://localhost:3000/ +``` + +You can also write tests manually following these suggested best practices: + +1. Use the right browser context: Playwright provides three browser options: `chromium`, `firefox`, and `webkit`. Choose the browser that best suits your needs in terms of features, performance, and compatibility. + +2. Close browser instances: Always close the browser instances and associated resources using the `close()` method. Failing to close the browser can lead to memory leaks and unexpected behavior. + +3. Reuse browser contexts: Reusing browser contexts can improve performance. Instead of creating a new context for each new page, consider creating a shared context and reusing it across multiple pages. + +4. Use the `waitFor` methods: Playwright offers `waitFor` methods (e.g., `waitForSelector`, `waitForNavigation`) that allow you to wait for specific conditions before proceeding with further actions. This helps ensure that the page has fully loaded or the desired element is available before interacting with it. + +5. Emulate network conditions: Playwright allows you to emulate various network conditions, such as slow connections or offline mode, using the `context.route` and `context.routeOverride` methods. This can be helpful for testing how your application behaves under different network scenarios. + +6. Handle errors and timeouts: Playwright operations can sometimes fail due to network issues, element unavailability, or other reasons. Properly handle errors and timeouts by using `try-catch` blocks and setting appropriate timeout values for operations like navigation or element waiting. + +7. Use `click` and `type` with caution: While using `click` and `type` methods, make sure to target the correct element and account for any potential delays caused by JavaScript events or animations on the page. + +8. Configure viewport and device emulation: Playwright allows you to set the viewport size and emulate different devices using the `page.setViewportSize` and `page.emulate` methods. Adjusting the viewport and device emulation can help test the responsiveness of your application. + +9. Use selective screenshotting: Capture screenshots strategically to minimize resource usage. Avoid taking excessive screenshots or capturing unnecessary parts of the page unless required for debugging or reporting. + +10. Run in headless mode: Consider running Playwright in headless mode (`headless: true`) for improved performance and resource utilization, especially in production or non-visual testing scenarios. + +11. Follow Playwright documentation: Playwright has comprehensive documentation with detailed guides, examples, and API references. Consult the official Playwright documentation (https://playwright.dev/) for specific use cases, best practices, and updates. + +**Running tests** + +Running tests can be completed using the package.json\scripts as follows: + +Testing end to end: + +``` +pnpm run test:e2e +``` + +Testing i18n: + +``` +pnpm run test:i18n +``` + +Testing GCS file upload: + +``` +pnpm run test:gcs +``` + +**Note**: `pnpm run test:gcs` will run a script `scripts/tests/test-gcs.sh` that configures GOOGLE_APPLICATION_CREDENTIALS from service-account-key.json information stored as a stringify JSON object in `scripts\tests\.env`, for use of the [service account](https://console.cloud.google.com/iam-admin/serviceaccounts/details/106707473171516793046?project=emissions-elt-demo) permisions for GCS authentication. + + ## Running App Locally ### run dev server @@ -359,5 +429,3 @@ pnpm run k8s ``` The output of `k8s` will be displayed in the terminal to confirm failure or success of setting a Kubernetes secret using shell "scripts\k8s-secrets.sh"; after which, Cloud Code\Run Kubernetes should launch - -gitleaks badge diff --git a/app/[lng]/analyst/analytic/page.tsx b/app/[lng]/analyst/analytic/page.tsx index 53ece13..2a62459 100644 --- a/app/[lng]/analyst/analytic/page.tsx +++ b/app/[lng]/analyst/analytic/page.tsx @@ -1,4 +1,8 @@ -import Analytic from "@/components/routes/Analytic"; +import dynamic from "next/dynamic"; +//๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server +const Analytic = dynamic(() => import("@/app/components/routes/Analytic"), { + ssr: false, +}); export default function Page() { return ( <> diff --git a/app/[lng]/analyst/anonymized/[id]/page.tsx b/app/[lng]/analyst/anonymized/[id]/page.tsx index d44da18..09757eb 100644 --- a/app/[lng]/analyst/anonymized/[id]/page.tsx +++ b/app/[lng]/analyst/anonymized/[id]/page.tsx @@ -7,13 +7,10 @@ export default function Page({ id: string; }; }) { - // ๐Ÿ‘‡๏ธ graphQL query endpoint for this role - const endpoint = "api/analyst/graphql"; - return ( <> {/* @ts-expect-error Server Component */} - + ); } diff --git a/app/[lng]/analyst/anonymized/page.tsx b/app/[lng]/analyst/anonymized/page.tsx index fa77d3d..e0d0d45 100644 --- a/app/[lng]/analyst/anonymized/page.tsx +++ b/app/[lng]/analyst/anonymized/page.tsx @@ -1,12 +1,10 @@ import Anonymized from "@/components/routes/anonymized/Anonymized"; export default function Page() { - // ๐Ÿ‘‡๏ธ graphQL query endpoint for this role - const endpoint = "api/analyst/graphql"; return ( <> {/* @ts-expect-error Server Component */} - + ); } diff --git a/app/[lng]/analyst/imported/[id]/page.tsx b/app/[lng]/analyst/imported/[id]/page.tsx new file mode 100644 index 0000000..326aac0 --- /dev/null +++ b/app/[lng]/analyst/imported/[id]/page.tsx @@ -0,0 +1,16 @@ +import ImportedArea from "@/components/routes/imported/id/Area"; + +export default function Page({ + params, +}: { + params: { + id: string; + }; +}) { + return ( + <> + {/* @ts-expect-error Server Component */} + + + ); +} diff --git a/app/[lng]/analyst/imported/page.tsx b/app/[lng]/analyst/imported/page.tsx index 9bbe3a7..e4df39b 100644 --- a/app/[lng]/analyst/imported/page.tsx +++ b/app/[lng]/analyst/imported/page.tsx @@ -1,12 +1,10 @@ import Imported from "@/components/routes/imported/Imported"; export default function Page() { - // ๐Ÿ‘‡๏ธ graphQL query endpoint for this role - const endpoint = "api/analyst/graphql"; return ( <> {/* @ts-expect-error Server Component */} - + ); } diff --git a/app/[lng]/auth/signin/page.tsx b/app/[lng]/auth/signin/page.tsx index 72b56d6..4d716a3 100644 --- a/app/[lng]/auth/signin/page.tsx +++ b/app/[lng]/auth/signin/page.tsx @@ -1,71 +1,12 @@ -"use client"; -import { getProviders, signIn, ClientSafeProvider } from "next-auth/react"; -import React, { useEffect, useState } from "react"; -import styles from "./styles.module.css"; -import { useTranslation } from "@/i18n/client"; import dynamic from "next/dynamic"; //๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server -const Tag = dynamic(() => import("@/components/layout/Tag"), { +const SignIn = dynamic(() => import("@/components/auth/SignIn"), { ssr: false, }); export default function Page() { - const { t } = useTranslation("translation"); - const [data, setData] = useState | null>( - null - ); - - // ๐Ÿ‘‡๏ธ code running on the client-side should be placed inside a useEffect hook with the appropriate condition to ensure it only runs in the browser - useEffect(() => { - // ๐Ÿ‘‡๏ธ call to next-auth providers list - const fetchData = async () => { - const providers = await getProviders(); - setData(providers); - }; - - fetchData(); - }, []); - - // ๐Ÿ‘‡๏ธ render the providers as login buttons with the correct calback url - let hostUrl; - if (typeof window !== "undefined") { - hostUrl = window.location.origin; - } - // ๐Ÿ‘‡๏ธ nextauth signin calback url - const callbackUrl = - hostUrl && hostUrl.includes("http://localhost:4503") - ? "http://localhost:3000" - : process.env.NEXTAUTH_URL || "http://localhost:3000"; - - // ๐Ÿ‘‡๏ธ nextauth provider signin - const handleSignIn = async (providerId: string) => { - await signIn(providerId, { - callbackUrl, - }); - }; - - const content = data - ? Object.values(data).map((provider: ClientSafeProvider) => ( -
- -
- )) - : null; - return ( <> - - {content} + ); } diff --git a/app/[lng]/error.tsx b/app/[lng]/error.tsx index 438555f..d5905ca 100644 --- a/app/[lng]/error.tsx +++ b/app/[lng]/error.tsx @@ -1,11 +1,26 @@ "use client"; import { useTranslation } from "@/i18n/client"; +import React, { useEffect, useState } from "react"; +const NoSSR: React.FC = ({ children }) => { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + return isMounted ? <>{children} : null; +}; + export default async function Error() { // ๐Ÿ‘‡๏ธ language management const { t } = useTranslation("translation"); return ( - <> -

{t("messages.errors.error")}

- +
+ + {/* Code that should not be translated on the server side */}; +

{t("messages.errors.error")}

+
+ {/* The rest of page content */} +
); } diff --git a/app/[lng]/unauth/page.tsx b/app/[lng]/unauth/page.tsx index 7577527..d59b6ed 100644 --- a/app/[lng]/unauth/page.tsx +++ b/app/[lng]/unauth/page.tsx @@ -1,12 +1,11 @@ -"use client"; -import { useTranslation } from "@/i18n/client"; +import { useTranslation } from "@/i18n"; -export default function Page() { - // ๐Ÿ‘‡๏ธ client language management - const { t } = useTranslation("translation"); +export default async function Page() { + // ๐Ÿ‘‡๏ธ language management + const { i18n } = await useTranslation(); return ( <> -

โ›”๏ธ {t("messages.errors.unauth")}

+

{i18n.t("messages.errors.unauth")}

); } diff --git a/app/api/analyst/upload/route.ts b/app/api/analyst/upload/route.ts index bd0e2a5..397a88d 100644 --- a/app/api/analyst/upload/route.ts +++ b/app/api/analyst/upload/route.ts @@ -1,3 +1,3 @@ -import handler from "@/utils/upload/post"; +import handler from "@/app/utils/api/upload/post"; export { handler as POST }; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 5facd2a..572fb89 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,4 +1,4 @@ -import { authOptions } from "@/utils/auth/auth"; +import { authOptions } from "@/utils/api/auth/auth"; import NextAuth from "next-auth"; const handler = NextAuth(authOptions); diff --git a/app/api/hello/route.ts b/app/api/hello/route.ts index ca38e27..d2d4ed4 100644 --- a/app/api/hello/route.ts +++ b/app/api/hello/route.ts @@ -9,3 +9,11 @@ export async function GET() { return new Response("Hello, this is a new API route"); } +import { NextResponse } from "next/server"; + +export async function POST() { + const res = await fetch("https://data.mongodb-api.com/..."); + const data = await res.json(); + + return NextResponse.json({ data }); +} diff --git a/app/components/auth/SignIn.tsx b/app/components/auth/SignIn.tsx new file mode 100644 index 0000000..e112a69 --- /dev/null +++ b/app/components/auth/SignIn.tsx @@ -0,0 +1,69 @@ +"use client"; +import { getProviders, signIn, ClientSafeProvider } from "next-auth/react"; +import React, { useEffect, useState } from "react"; +import styles from "./styles.module.css"; +import { useTranslation } from "@/i18n/client"; +import Tag from "@/components/layout/Tag"; + +export default function Page() { + const { t } = useTranslation("translation"); + const [data, setData] = useState | null>( + null + ); + + // ๐Ÿ‘‡๏ธ code running on the client-side should be placed inside a useEffect hook with the appropriate condition to ensure it only runs in the browser + useEffect(() => { + // ๐Ÿ‘‡๏ธ call to next-auth providers list + const fetchData = async () => { + const providers = await getProviders(); + setData(providers); + }; + + fetchData(); + }, []); + + // ๐Ÿ‘‡๏ธ render the providers as login buttons with the correct calback url + let hostUrl; + if (typeof window !== "undefined") { + hostUrl = window.location.origin; + } + // ๐Ÿ‘‡๏ธ nextauth signin calback url + const callbackUrl = + hostUrl && hostUrl.includes("http://localhost:4503") + ? "http://localhost:3000" + : process.env.NEXTAUTH_URL || "http://localhost:3000"; + + // ๐Ÿ‘‡๏ธ nextauth provider signin + const handleSignIn = async (providerId: string) => { + await signIn(providerId, { + callbackUrl, + }); + }; + + const content = data + ? Object.values(data).map((provider: ClientSafeProvider) => ( +
+ +
+ )) + : null; + + return ( + <> + + {content} + + ); +} diff --git a/app/[lng]/auth/signin/styles.module.css b/app/components/auth/styles.module.css similarity index 100% rename from app/[lng]/auth/signin/styles.module.css rename to app/components/auth/styles.module.css diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 48ed41e..5222ede 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -24,4 +24,4 @@ export default function Header() { ); -} \ No newline at end of file +} diff --git a/app/components/query/DataTableQuery.tsx b/app/components/query/DataTableQuery.tsx index 49b3a91..8c1a1ca 100644 --- a/app/components/query/DataTableQuery.tsx +++ b/app/components/query/DataTableQuery.tsx @@ -1,4 +1,4 @@ -import { getQueryData } from "@/utils/helpers"; +import { getQueryData } from "@/utils/postgraphile/helpers"; import { GraphqlQuery } from "@/types/declarations"; import dynamic from "next/dynamic"; //๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server diff --git a/app/components/routes/anonymized/Anonymized.tsx b/app/components/routes/anonymized/Anonymized.tsx index 06931f6..f974fee 100644 --- a/app/components/routes/anonymized/Anonymized.tsx +++ b/app/components/routes/anonymized/Anonymized.tsx @@ -1,41 +1,43 @@ +import { getSessionRoleEndpoint } from "@/utils/postgraphile/helpers"; import { Suspense } from "react"; import { gql } from "graphql-request"; import Spinner from "@/components/common/Spinner"; import DataTableQuery from "@/components/query/DataTableQuery"; import { columnsAnonymized } from "@/utils/table/columns"; import { crumbsAnonymized } from "@/utils/navigation/crumbs"; -import { GraphqlEndPoint } from "@/types/declarations"; import dynamic from "next/dynamic"; //๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server const Tag = dynamic(() => 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 changes options for @/components/table/DataTable const cntx = "anonymized"; -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/anonymized/id/Area.tsx b/app/components/routes/anonymized/id/Area.tsx index 71ad615..7d7b827 100644 --- a/app/components/routes/anonymized/id/Area.tsx +++ b/app/components/routes/anonymized/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"; @@ -11,7 +12,10 @@ import dynamic from "next/dynamic"; const Tag = dynamic(() => import("@/components/layout/Tag"), { ssr: false, }); -export default async function Page({ id, endpoint }: GraphqlParamEndPoint) { +export default async function Page({ id }: GraphqlParamEndPoint) { + // ๐Ÿ‘‡๏ธ role base graphQL api route + const endpoint = await getSessionRoleEndpoint(); + // ๐Ÿ‘‡๏ธ graphQL query const query = gql` diff --git a/app/components/routes/dataset/Add.tsx b/app/components/routes/dataset/Add.tsx index 144543c..2a85985 100644 --- a/app/components/routes/dataset/Add.tsx +++ b/app/components/routes/dataset/Add.tsx @@ -83,6 +83,8 @@ export default function Page({ endpoint }: GraphqlEndPoint) { setErrorMessage(errorMessage); } } + // Return the response or any relevant data + return response; } catch (error: any) { setErrorMessage(`An error occurred while uploading: ${error.message}`); } finally { @@ -99,7 +101,9 @@ export default function Page({ endpoint }: GraphqlEndPoint) { return ( <> {/* Mask overlay */} - {isMasked &&
} + {isMasked && ( +
+ )} {/* Tiles */}
@@ -114,7 +118,11 @@ export default function Page({ endpoint }: GraphqlEndPoint) {

{t("dataset.add.request")}

-
handleClickInputFile()}> +
handleClickInputFile()} + >
{t("dataset.add.file") 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 }; } - -