From 6cc3652ef8ef25f33ffb245d7d09978d66c39ade Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Tue, 11 Feb 2025 16:10:51 -0600 Subject: [PATCH 01/21] enforce MFA for all accounts Redirects to /account/mfa whenever a user without mfa enabled uses SI --- next.config.mjs | 6 + package-lock.json | 741 ++++++++++++++++--------------- package.json | 5 +- src/app/account/mfa/add/page.tsx | 154 +++++++ src/app/account/mfa/page.tsx | 108 +++++ src/components/errors.tsx | 9 +- src/middleware.ts | 14 +- types/globals.d.ts | 7 + 8 files changed, 687 insertions(+), 357 deletions(-) create mode 100644 src/app/account/mfa/add/page.tsx create mode 100644 src/app/account/mfa/page.tsx create mode 100644 types/globals.d.ts diff --git a/next.config.mjs b/next.config.mjs index e5bbfda1..64520461 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -43,4 +43,10 @@ export default withSentryConfig(nextConfig, { // https://docs.sentry.io/product/crons/ // https://vercel.com/docs/cron-jobs automaticVercelMonitors: true, + + experimental: { + turbo: { + // ... + }, + }, }) diff --git a/package-lock.json b/package-lock.json index ed8aad51..bb30498d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "next-swagger-doc": "^0.4", "papaparse": "^5.5.2", "pg": "^8.13.1", + "qrcode.react": "^4.2.0", "react": "19.0.0", "react-dom": "19.0.0", "react-icons": "^5.4.0", @@ -66,7 +67,6 @@ "@types/debug": "^4.1.12", "@types/react": "19.0.8", "@vitejs/plugin-react": "^4.3.4", - "@vitest/coverage-v8": "^3.0.5", "dotenv": "*", "eslint": "^9.19.0", "eslint-config-next": "*", @@ -349,45 +349,45 @@ } }, "node_modules/@aws-sdk/client-ecr": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.741.0.tgz", - "integrity": "sha512-YVqhAsP3c1AHz+BhWdtQqzZ+XQvilYLruU1CuDjhxYgItbUow7Kwkm0PHyZSuHC6GdRsboDD8m/tvKU8RXTxcA==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.744.0.tgz", + "integrity": "sha512-9IdP+pa5BUlBmEE9ie91rFjpsFLv9c9ho9rWZ8mr5m4ng6OHEcl11WQPbjHClYgV6qaaCn+0kXwdBHchL+f4Yw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.744.0", + "@aws-sdk/credential-provider-node": "3.744.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.744.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.744.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.3", + "@smithy/middleware-retry": "^4.0.4", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.4", + "@smithy/util-defaults-mode-node": "^4.0.4", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -400,35 +400,35 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.741.0.tgz", - "integrity": "sha512-sZvdbRZ+E9/GcOMUOkZvYvob95N6c9LdzDneXHFASA7OIaEOQxQT1Arimz7JpEhfq/h9K2/j7wNO4jh4x80bmA==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.744.0.tgz", + "integrity": "sha512-UuiqxVI5FKlnNcWoDP8bsyJcMJa7XjGcCbVCfKSpSboNeBM4tQS3ZIViSYuz+BeO8/MuwCy7hKn7+Zjivit1nA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.744.0", + "@aws-sdk/credential-provider-node": "3.744.0", "@aws-sdk/middleware-bucket-endpoint": "3.734.0", "@aws-sdk/middleware-expect-continue": "3.734.0", - "@aws-sdk/middleware-flexible-checksums": "3.735.0", + "@aws-sdk/middleware-flexible-checksums": "3.744.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-location-constraint": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-sdk-s3": "3.740.0", + "@aws-sdk/middleware-sdk-s3": "3.744.0", "@aws-sdk/middleware-ssec": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.744.0", "@aws-sdk/region-config-resolver": "3.734.0", - "@aws-sdk/signature-v4-multi-region": "3.740.0", + "@aws-sdk/signature-v4-multi-region": "3.744.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.744.0", "@aws-sdk/xml-builder": "3.734.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/eventstream-serde-browser": "^4.0.1", "@smithy/eventstream-serde-config-resolver": "^4.0.1", "@smithy/eventstream-serde-node": "^4.0.1", @@ -439,21 +439,21 @@ "@smithy/invalid-dependency": "^4.0.1", "@smithy/md5-js": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.3", + "@smithy/middleware-retry": "^4.0.4", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.4", + "@smithy/util-defaults-mode-node": "^4.0.4", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -467,44 +467,44 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.734.0.tgz", - "integrity": "sha512-oerepp0mut9VlgTwnG5Ds/lb0C0b2/rQ+hL/rF6q+HGKPfGsCuPvFx1GtwGKCXd49ase88/jVgrhcA9OQbz3kg==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.744.0.tgz", + "integrity": "sha512-mzJxPQ9mcnNY50pi7+pxB34/Dt7PUn0OgkashHdJPTnavoriLWvPcaQCG1NEVAtyzxNdowhpi4KjC+aN1EwAeA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.744.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.744.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.3", + "@smithy/middleware-retry": "^4.0.4", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.4", + "@smithy/util-defaults-mode-node": "^4.0.4", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -516,45 +516,45 @@ } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.741.0.tgz", - "integrity": "sha512-jvH4VQp5y9s2lo/l5Vh1gDW9viZ+hYcBUAknHp5GvZYeROMgH3xsbUXhaiFlhwv2/mOJpeukreuQthOkZYEsQA==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.744.0.tgz", + "integrity": "sha512-JvSsbqaeBa0EHO7ajWHtM66wP5IK1uNyLJBUYvaHxxe3Zsg4LFzdogXHcnxLcxlLXun6w3bi6tWd0kD2NYm3Yg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-node": "3.741.0", + "@aws-sdk/core": "3.744.0", + "@aws-sdk/credential-provider-node": "3.744.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.744.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.744.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.3", + "@smithy/middleware-retry": "^4.0.4", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.4", + "@smithy/util-defaults-mode-node": "^4.0.4", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -566,18 +566,18 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.734.0.tgz", - "integrity": "sha512-SxnDqf3vobdm50OLyAKfqZetv6zzwnSqwIwd3jrbopxxHKqNIM/I0xcYjD6Tn+mPig+u7iRKb9q3QnEooFTlmg==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.744.0.tgz", + "integrity": "sha512-R0XLfDDq7MAXYyDf7tPb+m0R7gmzTRRDtPNQ5jvuq8dbkefph5gFMkxZ2zSx7dfTsfYHhBPuTBsQ0c5Xjal3Vg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.734.0", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/node-config-provider": "^4.0.1", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/util-middleware": "^4.0.1", "fast-xml-parser": "4.4.1", @@ -588,12 +588,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.734.0.tgz", - "integrity": "sha512-gtRkzYTGafnm1FPpiNO8VBmJrYMoxhDlGPYDVcijzx3DlF8dhWnowuSBCxLSi+MJMx5hvwrX2A+e/q0QAeHqmw==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.744.0.tgz", + "integrity": "sha512-hyjC7xqzAeERorYYjhQG1ivcr1XlxgfBpa+r4pG29toFG60mACyVzaR7+og3kgzjRFAB7D1imMxPQyEvQ1QokA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -604,18 +604,18 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.734.0.tgz", - "integrity": "sha512-JFSL6xhONsq+hKM8xroIPhM5/FOhiQ1cov0lZxhzZWj6Ai3UAjucy3zyIFDr9MgP1KfCYNdvyaUq9/o+HWvEDg==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.744.0.tgz", + "integrity": "sha512-k+P1Tl5ewBvVByR6hB726qFIzANgQVf2cY87hZ/e09pQYlH4bfBcyY16VJhkqYnKmv6HMdWxKHX7D8nwlc8Obg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/property-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/util-stream": "^4.0.2", "tslib": "^2.6.2" @@ -625,18 +625,18 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.741.0.tgz", - "integrity": "sha512-/XvnVp6zZXsyUlP1FtmspcWnd+Z1u2WK0wwzTE/x277M0oIhAezCW79VmcY4jcDQbYH+qMbtnBexfwgFDARxQg==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.744.0.tgz", + "integrity": "sha512-hjEWgkF86tkvg8PIsDiB3KkTj7z8ZFGR0v0OLQYD47o17q1qfoMzZmg9wae3wXp9KzU+lZETo+8oMqX9a+7aVQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/credential-provider-env": "3.734.0", - "@aws-sdk/credential-provider-http": "3.734.0", - "@aws-sdk/credential-provider-process": "3.734.0", - "@aws-sdk/credential-provider-sso": "3.734.0", - "@aws-sdk/credential-provider-web-identity": "3.734.0", - "@aws-sdk/nested-clients": "3.734.0", + "@aws-sdk/core": "3.744.0", + "@aws-sdk/credential-provider-env": "3.744.0", + "@aws-sdk/credential-provider-http": "3.744.0", + "@aws-sdk/credential-provider-process": "3.744.0", + "@aws-sdk/credential-provider-sso": "3.744.0", + "@aws-sdk/credential-provider-web-identity": "3.744.0", + "@aws-sdk/nested-clients": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", @@ -649,17 +649,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.741.0.tgz", - "integrity": "sha512-iz/puK9CZZkZjrKXX2W+PaiewHtlcD7RKUIsw4YHFyb8lrOt7yTYpM6VjeI+T//1sozjymmAnnp1SST9TXApLQ==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.744.0.tgz", + "integrity": "sha512-4oUfRd6pe/VGmKoav17pPoOO0WP0L6YXmHqtJHSDmFUOAa+Vh0ZRljTj/yBdleRgdO6rOfdWqoGLFSFiAZDrsQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.734.0", - "@aws-sdk/credential-provider-http": "3.734.0", - "@aws-sdk/credential-provider-ini": "3.741.0", - "@aws-sdk/credential-provider-process": "3.734.0", - "@aws-sdk/credential-provider-sso": "3.734.0", - "@aws-sdk/credential-provider-web-identity": "3.734.0", + "@aws-sdk/credential-provider-env": "3.744.0", + "@aws-sdk/credential-provider-http": "3.744.0", + "@aws-sdk/credential-provider-ini": "3.744.0", + "@aws-sdk/credential-provider-process": "3.744.0", + "@aws-sdk/credential-provider-sso": "3.744.0", + "@aws-sdk/credential-provider-web-identity": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/credential-provider-imds": "^4.0.1", "@smithy/property-provider": "^4.0.1", @@ -672,12 +672,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.734.0.tgz", - "integrity": "sha512-zvjsUo+bkYn2vjT+EtLWu3eD6me+uun+Hws1IyWej/fKFAqiBPwyeyCgU7qjkiPQSXqk1U9+/HG9IQ6Iiz+eBw==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.744.0.tgz", + "integrity": "sha512-m0d/pDBIaiEAAxWXt/c79RHsKkUkyPOvF2SAMRddVhhOt1GFZI4ml+3f4drmAZfXldIyJmvJTJJqWluVPwTIqQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -689,14 +689,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.734.0.tgz", - "integrity": "sha512-cCwwcgUBJOsV/ddyh1OGb4gKYWEaTeTsqaAK19hiNINfYV/DO9r4RMlnWAo84sSBfJuj9shUNsxzyoe6K7R92Q==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.744.0.tgz", + "integrity": "sha512-xdMufTZOvpbDoDPI2XLu0/Rg3qJ/txpS8IJR63NsCGotHJZ/ucLNKwTcGS40hllZB8qSHTlvmlOzElDahTtx/A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.734.0", - "@aws-sdk/core": "3.734.0", - "@aws-sdk/token-providers": "3.734.0", + "@aws-sdk/client-sso": "3.744.0", + "@aws-sdk/core": "3.744.0", + "@aws-sdk/token-providers": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -708,13 +708,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.734.0.tgz", - "integrity": "sha512-t4OSOerc+ppK541/Iyn1AS40+2vT/qE+MFMotFkhCgCJbApeRF2ozEdnDN6tGmnl4ybcUuxnp9JWLjwDVlR/4g==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.744.0.tgz", + "integrity": "sha512-cNk93GZxORzqEojWfXdrPBF6a7Nu3LpPCWG5mV+lH2tbuGsmw6XhKkwpt7o+OiIP4tKCpHlvqOD8f1nmhe1KDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", - "@aws-sdk/nested-clients": "3.734.0", + "@aws-sdk/core": "3.744.0", + "@aws-sdk/nested-clients": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -725,14 +725,14 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.741.0.tgz", - "integrity": "sha512-bbX3m5kdoa0zgo1DqyIDasolE8v/mo0X43unqW6g5hfMPI375X9QAFpn0bVmmcBQ62ixV8BO3aZLAPSZb1/IqQ==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.744.0.tgz", + "integrity": "sha512-+WCIqTc2rT92kwL42di/zoHhQBGmUWAz8tr1wMQdTf+XlTDJZ4KhsfPwNsh5Gdge7il2yWuNRtB7uiZCFTM3kQ==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/smithy-client": "^4.1.2", + "@smithy/middleware-endpoint": "^4.0.3", + "@smithy/smithy-client": "^4.1.3", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", @@ -742,7 +742,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.741.0" + "@aws-sdk/client-s3": "^3.744.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -779,15 +779,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.735.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.735.0.tgz", - "integrity": "sha512-Tx7lYTPwQFRe/wQEHMR6Drh/S+X0ToAEq1Ava9QyxV1riwtepzRLojpNDELFb3YQVVYbX7FEiBMCJLMkmIIY+A==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.744.0.tgz", + "integrity": "sha512-4AuBdvkwfwagZQt3kt1b0x2dtC54cOrN5gt96V2b4wIjHBRxB/IfAyynahOgx3fd7Zjf74xwmxasjs7iJ8yglg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.0.1", @@ -861,19 +861,19 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.740.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.740.0.tgz", - "integrity": "sha512-VML9TzNoQdAs5lSPQSEgZiPgMUSz2H7SltaLb9g4tHwKK5xQoTq5WcDd6V1d2aPxSN5Q2Q63aiVUBby6MdUN/Q==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.744.0.tgz", + "integrity": "sha512-zE0kNjMV7B8pC2ClhrV2gCj/gWLiinRkfPeiUevfjl+Hdke9zcAWVNHLeGV54FJjXQEdwIAjeE7WJdHo7hio7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-arn-parser": "3.723.0", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/node-config-provider": "^4.0.1", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.1", @@ -900,15 +900,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.734.0.tgz", - "integrity": "sha512-MFVzLWRkfFz02GqGPjqSOteLe5kPfElUrXZft1eElnqulqs6RJfVSpOV7mO90gu293tNAeggMWAVSGRPKIYVMg==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.744.0.tgz", + "integrity": "sha512-ROUbDQHfVWiBHXd4m9E9mKj1Azby8XCs8RC8OCf9GVH339GSE6aMrPJSzMlsV1LmzPdPIypgp5qqh5NfSrKztg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", - "@smithy/core": "^3.1.1", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.2", "@smithy/protocol-http": "^5.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" @@ -918,44 +918,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.734.0.tgz", - "integrity": "sha512-iph2XUy8UzIfdJFWo1r0Zng9uWj3253yvW9gljhtu+y/LNmNvSnJxQk1f3D2BC5WmcoPZqTS3UsycT3mLPSzWA==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.744.0.tgz", + "integrity": "sha512-Mnrlh4lRY1gZQnKvN2Lh/5WXcGkzC41NM93mtn2uaqOh+DZLCXCttNCfbUesUvYJLOo3lYaOpiDsjTkPVB1yjw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.734.0", + "@aws-sdk/core": "3.744.0", "@aws-sdk/middleware-host-header": "3.734.0", "@aws-sdk/middleware-logger": "3.734.0", "@aws-sdk/middleware-recursion-detection": "3.734.0", - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.744.0", "@aws-sdk/region-config-resolver": "3.734.0", "@aws-sdk/types": "3.734.0", - "@aws-sdk/util-endpoints": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", "@aws-sdk/util-user-agent-browser": "3.734.0", - "@aws-sdk/util-user-agent-node": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.744.0", "@smithy/config-resolver": "^4.0.1", - "@smithy/core": "^3.1.1", + "@smithy/core": "^3.1.2", "@smithy/fetch-http-handler": "^5.0.1", "@smithy/hash-node": "^4.0.1", "@smithy/invalid-dependency": "^4.0.1", "@smithy/middleware-content-length": "^4.0.1", - "@smithy/middleware-endpoint": "^4.0.2", - "@smithy/middleware-retry": "^4.0.3", - "@smithy/middleware-serde": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.3", + "@smithy/middleware-retry": "^4.0.4", + "@smithy/middleware-serde": "^4.0.2", "@smithy/middleware-stack": "^4.0.1", "@smithy/node-config-provider": "^4.0.1", "@smithy/node-http-handler": "^4.0.2", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "@smithy/url-parser": "^4.0.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.3", - "@smithy/util-defaults-mode-node": "^4.0.3", + "@smithy/util-defaults-mode-browser": "^4.0.4", + "@smithy/util-defaults-mode-node": "^4.0.4", "@smithy/util-endpoints": "^3.0.1", "@smithy/util-middleware": "^4.0.1", "@smithy/util-retry": "^4.0.1", @@ -984,15 +984,15 @@ } }, "node_modules/@aws-sdk/s3-presigned-post": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-presigned-post/-/s3-presigned-post-3.741.0.tgz", - "integrity": "sha512-zWGTVKT+RWXaPvkwr3zURvTau8NTfsAnCKCYqjkkKwWuntKNeS0XCbClrUSkIeGjlQNA/8B5Z3cXFFZbyJmQAg==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-presigned-post/-/s3-presigned-post-3.744.0.tgz", + "integrity": "sha512-BB8lVp4GM8udWJArV+gXHv1AiHvE34xyStNepX+G2hwkQ91IKRtaS/VxEybNU1DNBucTIh+muMM2dcdN21V9+Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-s3": "3.741.0", + "@aws-sdk/client-s3": "3.744.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-format-url": "3.734.0", - "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/middleware-endpoint": "^4.0.3", "@smithy/signature-v4": "^5.0.1", "@smithy/types": "^4.1.0", "@smithy/util-hex-encoding": "^4.0.0", @@ -1004,17 +1004,17 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.741.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.741.0.tgz", - "integrity": "sha512-qrYYS+XG6wRwNDt60tcFKDCkQoLiBHhNlHaUtsHwdmSnlwA4aIuxCGXMkuskX93FsoLUDpuxtA0MZth3JL36dw==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.744.0.tgz", + "integrity": "sha512-RDMiGOr9HOb+JE5yaidNObY5hG/ZXZRYOhphvOekW5nCiIof03BIArQwF7w1eYbDErJinLpLU+SbUx3Ln/9KIw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.740.0", + "@aws-sdk/signature-v4-multi-region": "3.744.0", "@aws-sdk/types": "3.734.0", "@aws-sdk/util-format-url": "3.734.0", - "@smithy/middleware-endpoint": "^4.0.2", + "@smithy/middleware-endpoint": "^4.0.3", "@smithy/protocol-http": "^5.0.1", - "@smithy/smithy-client": "^4.1.2", + "@smithy/smithy-client": "^4.1.3", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" }, @@ -1023,12 +1023,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.740.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.740.0.tgz", - "integrity": "sha512-w+psidN3i+kl51nQEV3V+fKjKUqcEbqUA1GtubruDBvBqrl5El/fU2NF3Lo53y8CfI9wCdf3V7KOEpHIqxHNng==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.744.0.tgz", + "integrity": "sha512-QyrAevGGwceM+knGfV5r2NvSAjI94PETu6u+Fxalf8F/ybpK7qn1va0w3cGDU68oRqC0JHfo53JXjm9yQokj9Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.740.0", + "@aws-sdk/middleware-sdk-s3": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/protocol-http": "^5.0.1", "@smithy/signature-v4": "^5.0.1", @@ -1040,12 +1040,12 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.734.0.tgz", - "integrity": "sha512-2U6yWKrjWjZO8Y5SHQxkFvMVWHQWbS0ufqfAIBROqmIZNubOL7jXCiVdEFekz6MZ9LF2tvYGnOW4jX8OKDGfIw==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.744.0.tgz", + "integrity": "sha512-v/1+lWkDCd60Ei6oyhJqli6mTsPEVepLoSMB50vHUVlJP0fzXu/3FMje90/RzeUoh/VugZQJCEv/NNpuC6wztg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "3.734.0", + "@aws-sdk/nested-clients": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/property-provider": "^4.0.1", "@smithy/shared-ini-file-loader": "^4.0.1", @@ -1082,9 +1082,9 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.734.0.tgz", - "integrity": "sha512-w2+/E88NUbqql6uCVAsmMxDQKu7vsKV0KqhlQb0lL+RCq4zy07yXYptVNs13qrnuTfyX7uPXkXrlugvK9R1Ucg==", + "version": "3.743.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz", + "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.734.0", @@ -1136,12 +1136,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.734.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.734.0.tgz", - "integrity": "sha512-c6Iinh+RVQKs6jYUFQ64htOU2HUXFQ3TVx+8Tu3EDF19+9vzWi9UukhIMH9rqyyEXIAkk9XL7avt8y2Uyw2dGA==", + "version": "3.744.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.744.0.tgz", + "integrity": "sha512-BJURjwIXhNa4heXkLC0+GcL+8wVXaU7JoyW6ckdvp93LL+sVHeR1d5FxXZHQW/pMI4E3gNlKyBqjKaT75tObNQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.744.0", "@aws-sdk/types": "3.734.0", "@smithy/node-config-provider": "^4.0.1", "@smithy/types": "^4.1.0", @@ -1534,13 +1534,13 @@ } }, "node_modules/@clerk/backend": { - "version": "1.23.11", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.23.11.tgz", - "integrity": "sha512-N5CYCnVbSVXUkQg9oAAAf9r/kfPmBGxMqjzslDC9Tl3rkXFTkWjkBNnToB6We2ySdrmqiQGR/+/c4mS9XRbaOQ==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.24.0.tgz", + "integrity": "sha512-DlOZ9pnCY77ngHKFZzC7ZImHBVjMf2whPLvnnBt4YXjkvuQ3m1v1tQHUXb8qqlwilptHU4/WzkOlXytez+iJ+A==", "license": "MIT", "dependencies": { - "@clerk/shared": "^2.20.18", - "@clerk/types": "^4.45.0", + "@clerk/shared": "^2.21.0", + "@clerk/types": "^4.45.1", "cookie": "1.0.2", "snakecase-keys": "8.0.1", "tslib": "2.4.1" @@ -1556,13 +1556,13 @@ "license": "0BSD" }, "node_modules/@clerk/clerk-react": { - "version": "5.22.10", - "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.22.10.tgz", - "integrity": "sha512-3tTUt+3w5E+4ePl+6q6ugNTgjyjQw000nouXRngCEorDC2545Lc2HTZ37n1W8WuTx2BN32Rerjf2UJDmpRqrYg==", + "version": "5.22.12", + "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.22.12.tgz", + "integrity": "sha512-afaatXlyBlG1zRSQyRGLKS7eJZn46WZhAyBAswfIahXBtf9rMTzT4gCj4vYUm5wtnypneOIl3RYy07CkCSRcCQ==", "license": "MIT", "dependencies": { - "@clerk/shared": "^2.20.18", - "@clerk/types": "^4.45.0", + "@clerk/shared": "^2.21.0", + "@clerk/types": "^4.45.1", "tslib": "2.4.1" }, "engines": { @@ -1580,15 +1580,15 @@ "license": "0BSD" }, "node_modules/@clerk/nextjs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.11.0.tgz", - "integrity": "sha512-5ssp5OmGO3kH0PvfNhEoQFdrMnn80AdCI+h0LSQUGkQuGAFyRXidwne6Lu3W8S5ATqiZLPS8A8RMhuP75D+5uQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.11.2.tgz", + "integrity": "sha512-n8pwaKIdkk/AGmadIyhfsOmFixFXrmjAL524B1zJ+L6nOApyXpn8XncNavkdkoiZaP9j6iyhJUOXFZZ7IhVaNQ==", "license": "MIT", "dependencies": { - "@clerk/backend": "^1.23.11", - "@clerk/clerk-react": "^5.22.10", - "@clerk/shared": "^2.20.18", - "@clerk/types": "^4.45.0", + "@clerk/backend": "^1.24.0", + "@clerk/clerk-react": "^5.22.12", + "@clerk/shared": "^2.21.0", + "@clerk/types": "^4.45.1", "crypto-js": "4.2.0", "server-only": "0.0.1", "tslib": "2.4.1" @@ -1609,13 +1609,13 @@ "license": "0BSD" }, "node_modules/@clerk/shared": { - "version": "2.20.18", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-2.20.18.tgz", - "integrity": "sha512-vSQLZSRFr62+YeE1KE/Xu/eWCrwYJRNA2KRyQYmb2VPleFNmOjtTNlO4xkMZ0eN6BLKxrBo9b0NVwu3L28FhKg==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-2.21.0.tgz", + "integrity": "sha512-8uszJbdyfpk/qmu4SoIiT3T79TgSJe2uRm0m2isRZeGWw5DQFlf/dAF3iGOj7p2ad+24SGYb4cXwrrRdYyO8KQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@clerk/types": "^4.45.0", + "@clerk/types": "^4.45.1", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", @@ -1639,16 +1639,16 @@ } }, "node_modules/@clerk/testing": { - "version": "1.4.20", - "resolved": "https://registry.npmjs.org/@clerk/testing/-/testing-1.4.20.tgz", - "integrity": "sha512-v116nYkASRWoo3TF5/AFbVFR2PS0cnS5jF2XP13qdkL225Ghbw0oe2P2kD7E4SeuGtgdKwER+EcjnQR8Gp6LNw==", + "version": "1.4.21", + "resolved": "https://registry.npmjs.org/@clerk/testing/-/testing-1.4.21.tgz", + "integrity": "sha512-x7CX6cbIxbcbsoOGRPV8cWKWLHKOGVSiqck2y747HfWa3ghjMmfTij5t25TWJFsUfmZQ0LJBbwI5Y/4bk2UlWA==", "dev": true, "license": "MIT", "dependencies": { - "@clerk/backend": "^1.23.11", - "@clerk/shared": "^2.20.18", - "@clerk/types": "^4.45.0", - "dotenv": "16.4.5" + "@clerk/backend": "^1.24.0", + "@clerk/shared": "^2.21.0", + "@clerk/types": "^4.45.1", + "dotenv": "16.4.7" }, "engines": { "node": ">=18.17.0" @@ -1666,26 +1666,13 @@ } } }, - "node_modules/@clerk/testing/node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/@clerk/types": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.45.0.tgz", - "integrity": "sha512-DSXPWq1xD01tzyHv7CugX2T/XfVUZX2xxQ92cs+JPTrGoqIYm+yjRWqOz1CVJ/76TbYMOrB0efCGOxEcNV/PQw==", + "version": "4.45.1", + "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.45.1.tgz", + "integrity": "sha512-lS3Q8Ih4CasMY3ed7u+bXdO/s0OF1DIbSUhzQSKpfluzUxxkkFlPiRKsefctL4cnGt4fOzyD+T9ebF0up6sUkA==", "license": "MIT", "dependencies": { - "csstype": "3.1.1" + "csstype": "3.1.3" }, "engines": { "node": ">=18.17.0" @@ -2193,9 +2180,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2243,9 +2230,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -2276,6 +2263,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@faker-js/faker": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.4.0.tgz", @@ -2924,9 +2924,9 @@ "license": "MIT" }, "node_modules/@mantine/core": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.16.2.tgz", - "integrity": "sha512-6dwFz+8HrOqFan7GezgpoWyZSCxedh10S8iILGVsc3GXiD4gzo+3VZndZKccktkYZ3GVC9E3cCS3SxbiyKSAVw==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.16.3.tgz", + "integrity": "sha512-cxhIpfd2i0Zmk9TKdejYAoIvWouMGhzK3OOX+VRViZ5HEjnTQCGl2h3db56ThqB6NfVPCno6BPbt5lwekTtmuQ==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.28", @@ -2937,30 +2937,30 @@ "type-fest": "^4.27.0" }, "peerDependencies": { - "@mantine/hooks": "7.16.2", + "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/dropzone": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.16.2.tgz", - "integrity": "sha512-iRZJI/zzRrsSES+dVdqHInXnuxHQ6a7YPBwIP1Td9pBdaVHqF6Nvd/I2OVQSYhseYTxFT5ythdw32wFeCgpRSg==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.16.3.tgz", + "integrity": "sha512-JWKmRMuV0DfgIQWvvtRfokaIopezg2AwxxcXrHs5xxxN1EfiTQWB+aQjz0ISwcAk1gtjLEKHowqsBNbna+BEKw==", "license": "MIT", "dependencies": { "react-dropzone-esm": "15.2.0" }, "peerDependencies": { - "@mantine/core": "7.16.2", - "@mantine/hooks": "7.16.2", + "@mantine/core": "7.16.3", + "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/form": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.16.2.tgz", - "integrity": "sha512-JZkLbZ7xWAZndPrxObkf10gjHj57x8yvI/vobjDhfWN3zFPTSWmSSF6yBE1FpITseOs3oR03hlkqG6EclK6g+g==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.16.3.tgz", + "integrity": "sha512-GqomUG2Ri5adxYsTU1S5IhKRPcqTG5JkPvMERns8PQAcUz/lvzsnk3wY1v4K5CEbCAdpimle4bSsZTM9g697vg==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2971,9 +2971,9 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.16.2.tgz", - "integrity": "sha512-ZFHQhDi9T+r6VR5NEeE47gigPPIAHVIKDOCWsCsbCqHc3yz5l8kiO2RdfUmsTKV2KD/AiXnAw4b6pjQEP58GOg==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.16.3.tgz", + "integrity": "sha512-B94FBWk5Sc81tAjV+B3dGh/gKzfqzpzVC/KHyBRWOOyJRqeeRbI/FAaJo4zwppyQo1POSl5ArdyjtDRrRIj2SQ==", "license": "MIT", "peer": true, "peerDependencies": { @@ -2981,37 +2981,37 @@ } }, "node_modules/@mantine/modals": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.16.2.tgz", - "integrity": "sha512-REwAV53Fcz021EE3zLyYdkdFlfG+b24y279Y+eA1jCCH9VMLivXL+gacrox4BcpzREsic9nGVInSNv3VJwPlAQ==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.16.3.tgz", + "integrity": "sha512-BJuDzRugK6xLbuFTTo8NLJumVvVmSYsNVcEtmlXOWTE3NkDGktBXGKo8V1B0XfJ9/d/rZw7HCE0p4i76MtA+bQ==", "license": "MIT", "peerDependencies": { - "@mantine/core": "7.16.2", - "@mantine/hooks": "7.16.2", + "@mantine/core": "7.16.3", + "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/notifications": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.16.2.tgz", - "integrity": "sha512-U342XWiiRI1NvOlLsI6PH/pSNe0rxNClJ2w5orvjOMXvaAfDe52mhnzRmtzRxYENp06++3b/G7MjPH+466rF9Q==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.16.3.tgz", + "integrity": "sha512-wtEME9kSYfXWYmAmQUZ8c+rwNmhdWRBaW1mlPdQsPkzMqkv4q6yy0IpgwcnuHStSG9EHaQBXazmVxMZJdEAWBQ==", "license": "MIT", "dependencies": { - "@mantine/store": "7.16.2", + "@mantine/store": "7.16.3", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.16.2", - "@mantine/hooks": "7.16.2", + "@mantine/core": "7.16.3", + "@mantine/hooks": "7.16.3", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/store": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.16.2.tgz", - "integrity": "sha512-9dEGLosrYSePlAwhfx3CxTLcWu2M98TtuYnelAiHEdNEkyafirvZxNt4paMoFXLKR1XPm5wdjDK7bdTaE0t7Og==", + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.16.3.tgz", + "integrity": "sha512-6M2M5+0BrRtnVv+PUmr04tY1RjPqyapaHplo90uK1NMhP/1EIqrwTL9KoEtCNCJ5pog1AQtu0bj0QPbqUvxwLg==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" @@ -3036,9 +3036,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.6.tgz", - "integrity": "sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==", + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.7.tgz", + "integrity": "sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3269,6 +3269,15 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/instrumentation": { "version": "0.57.1", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz", @@ -3455,6 +3464,15 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/instrumentation-ioredis": { "version": "0.47.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz", @@ -3723,6 +3741,15 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", @@ -3740,7 +3767,7 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", @@ -3749,6 +3776,15 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.29.0.tgz", + "integrity": "sha512-KZ1JsXcP2pqunfsJBNk+py6AJ5R6ZJ3yvM5Lhhf93rHPHvdDzgfMYPS4F7GNO3j/MVDCtfbttrkcpu7sl0Wu/Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/sql-common": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", @@ -5645,9 +5681,9 @@ } }, "node_modules/@tabler/icons": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.29.0.tgz", - "integrity": "sha512-VWNINymdmhay3MDvWVREmRwuWLSrX3YiInKvs5L4AHRF4bAfJabLlEReE0BW/XFsBt22ff8/C8Eam/LXlF97mA==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.30.0.tgz", + "integrity": "sha512-c8OKLM48l00u9TFbh2qhSODMONIzML8ajtCyq95rW8vzkWcBrKRPM61tdkThz2j4kd5u17srPGIjqdeRUZdfdw==", "license": "MIT", "funding": { "type": "github", @@ -5655,12 +5691,12 @@ } }, "node_modules/@tabler/icons-react": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.29.0.tgz", - "integrity": "sha512-jaa3b3j91CplY7TPgx/Gj/e+PcOnQgYiK6c5qtp1P0ytfKM5WPc1qtXyRLE3NcYlfxS2Pcst4YGy1vUML7SjbQ==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.30.0.tgz", + "integrity": "sha512-9KZ9D1UNAyjlLkkYp2HBPHdf6lAJ2aelDqh8YYAnnmLF3xwprWKxxW8+zw5jlI0IwdfN4XFFuzqePkaw+DpIOg==", "license": "MIT", "dependencies": { - "@tabler/icons": "3.29.0" + "@tabler/icons": "3.30.0" }, "funding": { "type": "github", @@ -6029,17 +6065,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", + "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/type-utils": "8.24.0", + "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6059,16 +6095,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", + "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "engines": { @@ -6084,14 +6120,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", + "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6102,14 +6138,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", + "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -6126,9 +6162,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", + "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", "dev": true, "license": "MIT", "engines": { @@ -6140,14 +6176,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", + "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6223,16 +6259,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", + "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6247,13 +6283,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", + "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6423,9 +6459,9 @@ } }, "node_modules/@vitest/runner/node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -6445,9 +6481,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -8415,9 +8451,9 @@ } }, "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -9070,18 +9106,18 @@ } }, "node_modules/eslint": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", - "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.19.0", + "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -10580,12 +10616,12 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.12.0.tgz", - "integrity": "sha512-yAgSE7GmtRcu4ZUSFX/4v69UGXwugFFSdIQJ14LHPOPPQrWv8Y7O9PHsw8Ovk7bKCLe4sjXMbZFqGFcLHpZ89w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.0.tgz", + "integrity": "sha512-YG86SYDtrL/Yu8JgfWb7kjQ0myLeT1whw6fs/ZHFkXFcbk9zJU9lOCsSJHpvaPumU11nN3US7NW6x1YTk+HrUA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.8.2", + "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" @@ -14098,6 +14134,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -14472,9 +14517,9 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.0.tgz", - "integrity": "sha512-/Tvpny/RVVicqlYTKwt/GtpZRsPG1CmJNhxVKGz+Sy/4MONfXCVNK69MFgGKdUt0/324q3ClI2dICcPgISrC8g==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.1.tgz", + "integrity": "sha512-fgZEz/t3FDrU9o7EhI+iNNq1pNNpJImOvX72HUd6RoFiw8MaKd8/gR5tLuc8A0G0e55LMbP6ImjnmXY6zrTmjw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -15694,9 +15739,9 @@ } }, "node_modules/swr": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", - "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz", + "integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==", "license": "MIT", "dependencies": { "dequal": "^2.0.3", @@ -16296,9 +16341,9 @@ } }, "node_modules/type-fest": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz", - "integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==", + "version": "4.34.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz", + "integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -16424,15 +16469,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", - "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.0.tgz", + "integrity": "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@typescript-eslint/utils": "8.23.0" + "@typescript-eslint/eslint-plugin": "8.24.0", + "@typescript-eslint/parser": "8.24.0", + "@typescript-eslint/utils": "8.24.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16723,9 +16768,9 @@ } }, "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -16806,9 +16851,9 @@ } }, "node_modules/vite-node/node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -17405,9 +17450,9 @@ } }, "node_modules/vitest/node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index f6dbae42..c6986945 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "next-swagger-doc": "^0.4", "papaparse": "^5.5.2", "pg": "^8.13.1", + "qrcode.react": "^4.2.0", "react": "19.0.0", "react-dom": "19.0.0", "react-icons": "^5.4.0", @@ -86,7 +87,6 @@ "@types/debug": "^4.1.12", "@types/react": "19.0.8", "@vitejs/plugin-react": "^4.3.4", - "@vitest/coverage-v8": "^3.0.5", "dotenv": "*", "eslint": "^9.19.0", "eslint-config-next": "*", @@ -107,5 +107,6 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "*", "vitest-monocart-coverage": "^3.0.0" - } + }, + "overrides": {} } diff --git a/src/app/account/mfa/add/page.tsx b/src/app/account/mfa/add/page.tsx new file mode 100644 index 00000000..0ee8da16 --- /dev/null +++ b/src/app/account/mfa/add/page.tsx @@ -0,0 +1,154 @@ +'use client' + +import { useUser } from '@clerk/nextjs' +import { TOTPResource } from '@clerk/types' +import Link from 'next/link' +import * as React from 'react' +import { QRCodeSVG } from 'qrcode.react' +import { GenerateBackupCodes } from '../page' +import { useForm } from '@mantine/form' +import { Button, TextInput, Title, Flex } from '@mantine/core' +import { errorToString, reportError } from '@/components/errors' + +type AddTotpSteps = 'add' | 'verify' | 'backupcodes' | 'success' + +type DisplayFormat = 'qr' | 'uri' + +function AddTotpScreen({ setStep }: { setStep: React.Dispatch> }) { + const { user } = useUser() + const [totp, setTOTP] = React.useState(undefined) + const [displayFormat, setDisplayFormat] = React.useState('qr') + + React.useEffect(() => { + void user + ?.createTOTP() + .then((totp: TOTPResource) => { + setTOTP(totp) + }) + .catch((err) => reportError(err, 'Error generating MFA')) + }, []) + + return ( + <> + + Add MFA + + + + {totp && displayFormat === 'qr' && ( + <> +
+ +
+ + + )} + + {totp && displayFormat === 'uri' && ( + <> +
+

{totp.uri}

+
+ + + )} + + +
+

Once you have set up your authentication app, verify your code

+ + + ) +} + +function VerifyTotpScreen({ setStep }: { setStep: React.Dispatch> }) { + const { user } = useUser() + + const form = useForm({ + mode: 'uncontrolled', + initialValues: { + code: '', + }, + validate: { + code: (c: string) => (String(Number(c)).length != 6 ? 'Code must be six digits' : null), + }, + }) + + const verifyTotp = async (e: { code: string }) => { + try { + await user?.verifyTOTP({ code: e.code }) + setStep('backupcodes') + } catch (err: unknown) { + form.setErrors({ code: errorToString(err) || 'Invalid Code' }) + } + } + + return ( + + Verify MFA code +
+ + + + + + +
+ ) +} + +function BackupCodeScreen({ setStep }: { setStep: React.Dispatch> }) { + return ( + <> +

Verification was a success!

+
+

+ Save this list of backup codes somewhere safe in case you need to access your account in an + emergency +

+ + +
+ + ) +} + +function SuccessScreen() { + return ( + <> +

Success!

+

You have successfully added TOTP MFA via an authentication application.

+ + + + + ) +} + +export default function AddMFaScreen() { + const [step, setStep] = React.useState('add') + const { isLoaded, user } = useUser() + + if (!isLoaded) return null + + if (!user) { + return

You must be logged in to access this page

+ } + + return ( + <> + {step === 'add' && } + {step === 'verify' && } + {step === 'backupcodes' && } + {step === 'success' && } + + ) +} diff --git a/src/app/account/mfa/page.tsx b/src/app/account/mfa/page.tsx new file mode 100644 index 00000000..38c7f5be --- /dev/null +++ b/src/app/account/mfa/page.tsx @@ -0,0 +1,108 @@ +'use client' + +import * as React from 'react' +import { useUser } from '@clerk/nextjs' +import Link from 'next/link' +import { Button, Title } from '@mantine/core' +import { BackupCodeResource } from '@clerk/types' +import { reportError } from '@/components/errors' + +const HasMFA = () => { + return ( +
+

+ You have successfully enabled TOTP on your account + + + +

+
+ ) +} + +const EnableTotp = () => { + return ( + + + + ) +} + +// Generate and display backup codes +export function GenerateBackupCodes() { + const { user } = useUser() + const [backupCodes, setBackupCodes] = React.useState(undefined) + + const [loading, setLoading] = React.useState(false) + + React.useEffect(() => { + if (backupCodes) { + return + } + + setLoading(true) + void user + ?.createBackupCode() + .then((backupCode: BackupCodeResource) => { + setBackupCodes(backupCode) + setLoading(false) + }) + .catch((err) => { + reportError(err, 'Failed to generate backup codes') + setLoading(false) + }) + }, [backupCodes, user]) + + if (loading) { + return

Loading...

+ } + + if (!backupCodes) { + return

There was a problem generating backup codes

+ } + + return ( +
    + {backupCodes.codes.map((code, index) => ( +
  1. {code}
  2. + ))} +
+ ) +} + +export default function ManageMFA() { + const { isLoaded, user } = useUser() + const [showNewCodes, setShowNewCodes] = React.useState(false) + + if (!isLoaded) return null + + if (!user) { + return

You must be logged in to access this page

+ } + + if (user.totpEnabled) return + + return ( + <> + MFA is required + + In order to use SafeInsights, your account must have MFA enabled + + + {/* Manage backup codes */} + {user.backupCodeEnabled && user.twoFactorEnabled && ( +
+

+ Generate new backup codes? - +

+
+ )} + {showNewCodes && ( + <> + + + + )} + + ) +} diff --git a/src/components/errors.tsx b/src/components/errors.tsx index 203cc1e6..7fb97d4d 100644 --- a/src/components/errors.tsx +++ b/src/components/errors.tsx @@ -23,17 +23,16 @@ export function isClerkApiError(error: unknown): error is ClerkAPIErrorResponse ) } -export const reportError = (error: unknown, title = 'An error occured') => { - const message = isClerkApiError(error) +export const errorToString = (error: unknown) => + isClerkApiError(error) ? error.errors.map((e) => `${e.message}: ${e.longMessage}`).join('\n') : JSON.stringify(error, null, 2) - console.error('Error:', message) - +export const reportError = (error: unknown, title = 'An error occured') => { notifications.show({ color: 'red', title, - message, + message: errorToString(error), }) } diff --git a/src/middleware.ts b/src/middleware.ts index d032b707..65572c35 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -38,12 +38,16 @@ const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) const OPENSTAX_ORG_SLUG = 'openstax' const SAFEINSIGHTS_ORG_SLUG = 'safe-insights' +const ANON_ROUTES: Array = ['/reset-password', '/signup'] + +const MFA_ROUTE = '/account/mfa' + // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware export default clerkMiddleware(async (auth, req) => { try { - const { userId, orgId, orgRole, orgSlug } = await auth() + const { userId, orgId, orgRole, orgSlug, sessionClaims } = await auth() if (!userId) { // Block unauthenticated access to protected routes @@ -60,6 +64,7 @@ export default clerkMiddleware(async (auth, req) => { const userRoles = { isAdmin: orgSlug === SAFEINSIGHTS_ORG_SLUG, isOpenStaxMember: orgSlug === OPENSTAX_ORG_SLUG, + hasMFA: !!sessionClaims?.hasMFA, get isMember() { return this.isOpenStaxMember && !this.isAdmin }, @@ -75,12 +80,17 @@ export default clerkMiddleware(async (auth, req) => { }) // Handle authentication redirects - if (req.nextUrl.pathname.startsWith('/reset-password') || req.nextUrl.pathname.startsWith('/signup')) { + if (ANON_ROUTES.find((r) => req.nextUrl.pathname.startsWith(r))) { if (userId) { return NextResponse.redirect(new URL('/', req.url)) } } + // if they don't have MFA and are not currently adding it, force them to do so + if (!userRoles.hasMFA && !req.nextUrl.pathname.startsWith(MFA_ROUTE)) { + return NextResponse.redirect(new URL('/account/mfa', req.url)) + } + // Route protection const routeProtection = { member: isMemberRoute(req) && !userRoles.isMember && !userRoles.isAdmin, diff --git a/types/globals.d.ts b/types/globals.d.ts new file mode 100644 index 00000000..81564f88 --- /dev/null +++ b/types/globals.d.ts @@ -0,0 +1,7 @@ +export {} + +declare global { + interface CustomJwtSessionClaims { + hasMFA?: boolean + } +} From e20d0e721b2d84d0a4c21474de7a0a435547af4a Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Tue, 11 Feb 2025 16:45:58 -0600 Subject: [PATCH 02/21] add sms pages --- src/app/account/mfa/{add => app}/page.tsx | 2 +- src/app/account/mfa/page.tsx | 33 ++-- src/app/account/mfa/sms/page.tsx | 199 ++++++++++++++++++++++ 3 files changed, 221 insertions(+), 13 deletions(-) rename src/app/account/mfa/{add => app}/page.tsx (98%) create mode 100644 src/app/account/mfa/sms/page.tsx diff --git a/src/app/account/mfa/add/page.tsx b/src/app/account/mfa/app/page.tsx similarity index 98% rename from src/app/account/mfa/add/page.tsx rename to src/app/account/mfa/app/page.tsx index 0ee8da16..59012f5f 100644 --- a/src/app/account/mfa/add/page.tsx +++ b/src/app/account/mfa/app/page.tsx @@ -26,7 +26,7 @@ function AddTotpScreen({ setStep }: { setStep: React.Dispatch reportError(err, 'Error generating MFA')) - }, []) + }, []) // eslint-disable-line react-hooks/exhaustive-deps return ( <> diff --git a/src/app/account/mfa/page.tsx b/src/app/account/mfa/page.tsx index 38c7f5be..2850b656 100644 --- a/src/app/account/mfa/page.tsx +++ b/src/app/account/mfa/page.tsx @@ -1,9 +1,9 @@ 'use client' import * as React from 'react' -import { useUser } from '@clerk/nextjs' +import { useClerk, useUser } from '@clerk/nextjs' import Link from 'next/link' -import { Button, Title } from '@mantine/core' +import { Button, Flex, Title } from '@mantine/core' import { BackupCodeResource } from '@clerk/types' import { reportError } from '@/components/errors' @@ -11,7 +11,7 @@ const HasMFA = () => { return (

- You have successfully enabled TOTP on your account + You have successfully enabled MFA on your account @@ -20,14 +20,6 @@ const HasMFA = () => { ) } -const EnableTotp = () => { - return ( - - - - ) -} - // Generate and display backup codes export function GenerateBackupCodes() { const { user } = useUser() @@ -71,6 +63,7 @@ export function GenerateBackupCodes() { } export default function ManageMFA() { + const { openUserProfile } = useClerk() const { isLoaded, user } = useUser() const [showNewCodes, setShowNewCodes] = React.useState(false) @@ -87,7 +80,23 @@ export default function ManageMFA() { MFA is required In order to use SafeInsights, your account must have MFA enabled - + + + + + + + {user.phoneNumbers.length ? ( + + + + ) : ( + +

You could use SMS MFA if you have a phone number entered on your account.

+ + + )} + {/* Manage backup codes */} {user.backupCodeEnabled && user.twoFactorEnabled && ( diff --git a/src/app/account/mfa/sms/page.tsx b/src/app/account/mfa/sms/page.tsx new file mode 100644 index 00000000..da943070 --- /dev/null +++ b/src/app/account/mfa/sms/page.tsx @@ -0,0 +1,199 @@ +'use client' + +import * as React from 'react' +import { useUser, useClerk } from '@clerk/nextjs' + +import { Button, Flex, Title } from '@mantine/core' +import { BackupCodeResource, PhoneNumberResource } from '@clerk/types' +import Link from 'next/link' + +// Display phone numbers reserved for MFA +const ManageMfaPhoneNumbers = () => { + const { user } = useUser() + + if (!user) return null + + // Check if any phone numbers are reserved for MFA + const mfaPhones = user.phoneNumbers + .filter((ph) => ph.verification.status === 'verified') + .filter((ph) => ph.reservedForSecondFactor) + .sort((ph: PhoneNumberResource) => (ph.defaultSecondFactor ? -1 : 1)) + + if (user.phoneNumbers.length === 0) { + return

There are currently no phone numbers on your account.

+ } + + return ( + <> +

Phone numbers reserved for MFA

+
    + {mfaPhones.map((phone) => { + return ( +
  • +

    + {phone.phoneNumber} {phone.defaultSecondFactor && '(Default)'} +

    +
    + +
    + + {!phone.defaultSecondFactor && ( +
    + +
    + )} + + {user.phoneNumbers.length > 1 && ( +
    + +
    + )} +
  • + ) + })} +
+ You have enabled MFA on your account + + + + + ) +} + +// Display phone numbers that are not reserved for MFA +const ManageAvailablePhoneNumbers = () => { + const { user } = useUser() + + if (!user) return null + + // Check if any phone numbers aren't reserved for MFA + const availableForMfaPhones = user.phoneNumbers + .filter((ph) => ph.verification.status === 'verified') + .filter((ph) => !ph.reservedForSecondFactor) + + // Reserve a phone number for MFA + const reservePhoneForMfa = async (phone: PhoneNumberResource) => { + // Set the phone number as reserved for MFA + await phone.setReservedForSecondFactor({ reserved: true }) + // Refresh the user information to reflect changes + await user.reload() + } + + if (availableForMfaPhones.length === 0) { + return

There are currently no verified phone numbers available to be reserved for MFA.

+ } + + return ( + <> +

Phone numbers that are not reserved for MFA

+ +
    + {availableForMfaPhones.map((phone) => { + return ( +
  • +

    {phone.phoneNumber}

    +
    + +
    + {user.phoneNumbers.length > 1 && ( +
    + +
    + )} +
  • + ) + })} +
+ + ) +} + +// Generate and display backup codes +function GenerateBackupCodes() { + const { user } = useUser() + const [backupCodes, setBackupCodes] = React.useState(undefined) + + const [loading, setLoading] = React.useState(false) + + React.useEffect(() => { + if (backupCodes) { + return + } + + setLoading(true) + void user + ?.createBackupCode() + .then((backupCode: BackupCodeResource) => { + setBackupCodes(backupCode) + setLoading(false) + }) + .catch((err) => { + // See https://clerk.com/docs/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + setLoading(false) + }) + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + if (loading) { + return

Loading...

+ } + + if (!backupCodes) { + return

There was a problem generating backup codes

+ } + + return ( +
    + {backupCodes.codes.map((code, index) => ( +
  1. {code}
  2. + ))} +
+ ) +} + +export default function ManageSMSMFA() { + const [showBackupCodes, setShowBackupCodes] = React.useState(false) + const { openUserProfile } = useClerk() + const { isLoaded, user } = useUser() + + if (!isLoaded) return null + + if (!user) { + return

You must be logged in to access this page

+ } + + return ( + <> + MFA using SMS + + + + + + + {/* Manage backup codes */} + {user.twoFactorEnabled && ( +
+

+ Generate new backup codes? -{' '} + +

+
+ )} + {showBackupCodes && ( + <> + + + + )} +
+ + ) +} From 92fa5477eb047a4afe92d0bf8f923a53f38e2d30 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 09:27:16 -0600 Subject: [PATCH 03/21] make signin a page and move all to under /account --- src/app/{ => account}/reset-password/page.tsx | 5 +- src/app/account/signin/page.tsx | 13 +++ src/app/{ => account}/signup/page.tsx | 5 +- src/app/page.tsx | 19 +-- src/components/nav-auth-menu.tsx | 5 +- src/components/signin-link.tsx | 16 +++ src/components/signin.tsx | 74 +++++++++++- src/middleware.ts | 109 ++++++++---------- 8 files changed, 164 insertions(+), 82 deletions(-) rename src/app/{ => account}/reset-password/page.tsx (62%) create mode 100644 src/app/account/signin/page.tsx rename src/app/{ => account}/signup/page.tsx (61%) create mode 100644 src/components/signin-link.tsx diff --git a/src/app/reset-password/page.tsx b/src/app/account/reset-password/page.tsx similarity index 62% rename from src/app/reset-password/page.tsx rename to src/app/account/reset-password/page.tsx index b51bb375..9c4f9d0f 100644 --- a/src/app/reset-password/page.tsx +++ b/src/app/account/reset-password/page.tsx @@ -1,10 +1,13 @@ import { ResetPassword } from '@/components/reset-password' import { pageStyles } from '@/styles/common' +import { Container } from '@/styles/generated/jsx' export default function ResetPasswordPage() { return (
- + + +
) } diff --git a/src/app/account/signin/page.tsx b/src/app/account/signin/page.tsx new file mode 100644 index 00000000..062bac1d --- /dev/null +++ b/src/app/account/signin/page.tsx @@ -0,0 +1,13 @@ +import { SignIn } from '@/components/signin' +import { pageStyles } from '@/styles/common' +import { Container } from '@/styles/generated/jsx' + +export default function Home() { + return ( +
+ + + +
+ ) +} diff --git a/src/app/signup/page.tsx b/src/app/account/signup/page.tsx similarity index 61% rename from src/app/signup/page.tsx rename to src/app/account/signup/page.tsx index 72bd0dee..057578ff 100644 --- a/src/app/signup/page.tsx +++ b/src/app/account/signup/page.tsx @@ -1,10 +1,13 @@ import { SignUp } from '@/components/signup' import { pageStyles } from '@/styles/common' +import { Container } from '@/styles/generated/jsx' export default function SignUpPage() { return (
- + + +
) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 2acac84b..fe320d06 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,23 +1,14 @@ -import { SignedIn, SignedOut } from '@clerk/nextjs' -import { SignIn } from '@/components/signin' -import { Title, Flex } from '@mantine/core' +import { Title } from '@mantine/core' import { UserNav } from './user-nav' import { pageStyles, mainStyles, footerStyles } from '@/styles/common' export default function Home() { return (
- - - - - - -
- Welcome to the SafeInsights management app. - -
-
+
+ Welcome to the SafeInsights management app. + +
A SafeInsights production
diff --git a/src/components/nav-auth-menu.tsx b/src/components/nav-auth-menu.tsx index 4357c750..23adfdcd 100644 --- a/src/components/nav-auth-menu.tsx +++ b/src/components/nav-auth-menu.tsx @@ -1,11 +1,12 @@ -import { SignInButton, SignedIn, SignedOut, UserButton, OrganizationSwitcher } from '@clerk/nextjs' +import { SignedIn, SignedOut, UserButton, OrganizationSwitcher } from '@clerk/nextjs' import { Group } from '@mantine/core' +import { SigninLink } from './signin-link' export const NavAuthMenu = () => { return ( <> - + diff --git a/src/components/signin-link.tsx b/src/components/signin-link.tsx new file mode 100644 index 00000000..3048cbec --- /dev/null +++ b/src/components/signin-link.tsx @@ -0,0 +1,16 @@ +'use client' + +import { usePathname } from 'next/navigation' +import Link from 'next/link' + +const PATHNAME = '/account/signin' + +export const SigninLink = () => { + const pathname = usePathname() + + if (pathname == PATHNAME) { + return null + } + + return Sign in +} diff --git a/src/components/signin.tsx b/src/components/signin.tsx index 03e7e6bc..0fb0fca8 100644 --- a/src/components/signin.tsx +++ b/src/components/signin.tsx @@ -1,29 +1,46 @@ 'use client' +import { useState } from 'react' import { isClerkApiError, reportError } from './errors' -import { Anchor, Button, Group, Loader, PasswordInput, Stack, Text, TextInput, Paper } from '@mantine/core' +import { Title, Anchor, Button, Group, Loader, PasswordInput, Stack, Text, TextInput, Paper } from '@mantine/core' import { isEmail, isNotEmpty, useForm } from '@mantine/form' import { useRouter } from 'next/navigation' import { useSignIn } from '@clerk/nextjs' +import { SignInResource } from '@clerk/types' + +const isUsingPhoneMFA = (signIn: SignInResource) => { + return Boolean( + signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'phone_code') && + !signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'totp'), + ) +} + +type MFAState = false | { usingSMS: boolean; signIn: SignInResource } export function SignIn() { const { isLoaded, signIn, setActive } = useSignIn() + + const [needsMFA, setNeedsMFA] = useState(false) + const router = useRouter() interface SignInFormValues { email: string password: string + code: string } const form = useForm({ initialValues: { email: '', + code: '', password: '', }, validate: { - email: isEmail('Invalid email'), - password: isNotEmpty('Required'), + code: needsMFA ? isNotEmpty('Required') : undefined, + email: needsMFA ? undefined : isEmail('Invalid email'), + password: needsMFA ? undefined : isNotEmpty('Required'), }, }) @@ -43,6 +60,13 @@ export function SignIn() { await setActive({ session: attempt.createdSessionId }) router.push('/') } + if (attempt.status === 'needs_second_factor') { + const usingSMS = isUsingPhoneMFA(attempt) + if (usingSMS) { + await attempt.prepareSecondFactor({ strategy: 'phone_code' }) + } + setNeedsMFA({ signIn: attempt, usingSMS }) + } } catch (err: unknown) { reportError(err, 'failed signin') if (isClerkApiError(err)) { @@ -54,6 +78,46 @@ export function SignIn() { } }) + const onMFA = form.onSubmit(async (values) => { + if (!isLoaded || !needsMFA) return + + const signInAttempt = await needsMFA.signIn.attemptSecondFactor({ + strategy: needsMFA.usingSMS ? 'phone_code' : 'totp', + code: values.code, + }) + + if (signInAttempt.status === 'complete') { + await setActive({ session: signInAttempt.createdSessionId }) + router.push('/') + } else { + reportError(`Unknown signIn status: ${signInAttempt.status}`) + } + }) + + if (needsMFA) { + return ( + + Enter MFA Code + Enter the code from {needsMFA.usingSMS ? 'the text message we sent' : 'your app'} + +
+ + + + + + + +
+ ) + } + return (
@@ -80,8 +144,8 @@ export function SignIn() { /> - Don't have an account? Sign Up Now - Forgot password? + Don't have an account? Sign Up Now + Forgot password?
diff --git a/src/middleware.ts b/src/middleware.ts index 65572c35..4157acd6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -38,7 +38,7 @@ const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) const OPENSTAX_ORG_SLUG = 'openstax' const SAFEINSIGHTS_ORG_SLUG = 'safe-insights' -const ANON_ROUTES: Array = ['/reset-password', '/signup'] +const ANON_ROUTES: Array = ['/account/reset-password', '/account/signup', '/account/signin'] const MFA_ROUTE = '/account/mfa' @@ -46,82 +46,73 @@ const MFA_ROUTE = '/account/mfa' // https://clerk.com/docs/references/nextjs/clerk-middleware export default clerkMiddleware(async (auth, req) => { - try { - const { userId, orgId, orgRole, orgSlug, sessionClaims } = await auth() - - if (!userId) { - // Block unauthenticated access to protected routes - if (isMemberRoute(req) || isResearcherRoute(req)) { - logger.warn('Access denied: Authentication required') - middlewareDebug('Blocking unauthenticated access to protected route') - return new NextResponse(null, { status: 403 }) - } - // For non-protected routes, let Clerk handle the redirect + + const { userId, orgId, orgRole, orgSlug, sessionClaims } = await auth() + + if (!userId) { + console.log('n o user', req.nextUrl.pathname) + if (ANON_ROUTES.find(r => req.nextUrl.pathname.startsWith(r))) { return NextResponse.next() } + return NextResponse.redirect(new URL('/account/signin', req.url)) + } - // Define user roles - const userRoles = { - isAdmin: orgSlug === SAFEINSIGHTS_ORG_SLUG, - isOpenStaxMember: orgSlug === OPENSTAX_ORG_SLUG, - hasMFA: !!sessionClaims?.hasMFA, - get isMember() { - return this.isOpenStaxMember && !this.isAdmin - }, - get isResearcher() { - return !this.isAdmin && !this.isOpenStaxMember - }, - } + // Define user roles + const userRoles = { + isAdmin: orgSlug === SAFEINSIGHTS_ORG_SLUG, + isOpenStaxMember: orgSlug === OPENSTAX_ORG_SLUG, + hasMFA: !!sessionClaims?.hasMFA, + get isMember() { + return this.isOpenStaxMember && !this.isAdmin + }, + get isResearcher() { + return !this.isAdmin && !this.isOpenStaxMember + }, + } - middlewareDebug('Auth check: %o', { - organization: orgId, - role: orgRole, - ...userRoles, - }) - - // Handle authentication redirects - if (ANON_ROUTES.find((r) => req.nextUrl.pathname.startsWith(r))) { - if (userId) { - return NextResponse.redirect(new URL('/', req.url)) - } - } + middlewareDebug('Auth check: %o', { + organization: orgId, + role: orgRole, + userId, + ...userRoles, + }) - // if they don't have MFA and are not currently adding it, force them to do so - if (!userRoles.hasMFA && !req.nextUrl.pathname.startsWith(MFA_ROUTE)) { - return NextResponse.redirect(new URL('/account/mfa', req.url)) - } + // if (!userId && !ANON_ROUTES.find((r) => req.nextUrl.pathname.startsWith(r))) { + // return NextResponse.redirect(new URL('/account/signin', req.url)) + // } - // Route protection - const routeProtection = { - member: isMemberRoute(req) && !userRoles.isMember && !userRoles.isAdmin, - researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin, - } + // if they don't have MFA and are not currently adding it, force them to do so + if (!userRoles.hasMFA && !req.nextUrl.pathname.startsWith(MFA_ROUTE)) { - if (routeProtection.member) { - logger.warn('Access denied: Member route requires member or admin access') - middlewareDebug('Blocking unauthorized member route access: %o', { userId, orgId, userRoles }) - return new NextResponse(null, { status: 403 }) - } + } - if (routeProtection.researcher) { - logger.warn('Access denied: Researcher route requires researcher or admin access') - middlewareDebug('Blocking unauthorized researcher route access: %o', { userId, orgId, userRoles }) - return new NextResponse(null, { status: 403 }) - } - } catch (error) { - logger.error('Middleware error:', error) + // Route protection + const routeProtection = { + member: isMemberRoute(req) && !userRoles.isMember && !userRoles.isAdmin, + researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin, + } + + if (routeProtection.member) { + logger.warn('Access denied: Member route requires member or admin access') + middlewareDebug('Blocking unauthorized member route access: %o', { userId, orgId, userRoles }) + return new NextResponse(null, { status: 403 }) + } + + if (routeProtection.researcher) { + logger.warn('Access denied: Researcher route requires researcher or admin access') + middlewareDebug('Blocking unauthorized researcher route access: %o', { userId, orgId, userRoles }) + return new NextResponse(null, { status: 403 }) } return NextResponse.next() }) + export const config = { matcher: [ // Skip Next.js internals and all static files, unless found in search params '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', // Always run for routes below '/(dl|member|researcher)(.*)', - '/', - '/(reset-password|signup)', ], } From 9357611d1227c17a231081a1813d3a49a339eda5 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 09:27:42 -0600 Subject: [PATCH 04/21] re-implement clerk signin helper to support mfa --- src/middleware.ts | 10 +----- tests/e2e.helpers.ts | 74 +++++++++++++++++++++++++++++++++++++------- vitest.config.ts | 2 +- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 4157acd6..5c642e96 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -46,12 +46,10 @@ const MFA_ROUTE = '/account/mfa' // https://clerk.com/docs/references/nextjs/clerk-middleware export default clerkMiddleware(async (auth, req) => { - const { userId, orgId, orgRole, orgSlug, sessionClaims } = await auth() if (!userId) { - console.log('n o user', req.nextUrl.pathname) - if (ANON_ROUTES.find(r => req.nextUrl.pathname.startsWith(r))) { + if (ANON_ROUTES.find((r) => req.nextUrl.pathname.startsWith(r))) { return NextResponse.next() } return NextResponse.redirect(new URL('/account/signin', req.url)) @@ -77,13 +75,8 @@ export default clerkMiddleware(async (auth, req) => { ...userRoles, }) - // if (!userId && !ANON_ROUTES.find((r) => req.nextUrl.pathname.startsWith(r))) { - // return NextResponse.redirect(new URL('/account/signin', req.url)) - // } - // if they don't have MFA and are not currently adding it, force them to do so if (!userRoles.hasMFA && !req.nextUrl.pathname.startsWith(MFA_ROUTE)) { - } // Route protection @@ -107,7 +100,6 @@ export default clerkMiddleware(async (auth, req) => { return NextResponse.next() }) - export const config = { matcher: [ // Skip Next.js internals and all static files, unless found in search params diff --git a/tests/e2e.helpers.ts b/tests/e2e.helpers.ts index d677980e..0a1e1098 100644 --- a/tests/e2e.helpers.ts +++ b/tests/e2e.helpers.ts @@ -1,6 +1,6 @@ import { type BrowserType, type Page, test as baseTest } from '@playwright/test' import { BLANK_UUID, db } from '@/database' -import { clerk, setupClerkTestingToken } from '@clerk/testing/playwright' +import { setupClerkTestingToken } from '@clerk/testing/playwright' import fs from 'fs' import path from 'path' @@ -12,6 +12,7 @@ export { expect, type Page } from '@playwright/test' export const USE_COVERAGE = process.argv.includes('--coverage') import { addCoverageReport } from 'monocart-reporter' +//import { useSignIn } from '@clerk/nextjs' export type CollectV8CodeCoverageOptions = { browserType: BrowserType @@ -89,19 +90,70 @@ export const test = baseTest.extend({ ], }) -type VisitClerkProtectedPageOptions = { url: string; role: 'researcher'; page: Page } -export const visitClerkProtectedPage = async ({ page, url }: VisitClerkProtectedPageOptions) => { +const clerkLoaded = async (page: Page) => { + await page.waitForFunction(() => window.Clerk !== undefined) + await page.waitForFunction(() => window.Clerk.loaded) +} + +// This function is serialized and executed in the browser context +// concept: https://github.com/clerk/javascript/blob/main/packages/testing/src/common/helpers-utils.ts#L6 +type ClerkSignInParams = { + password: string + identifier: string +} + +const clerkSignInHelper = async (params: ClerkSignInParams) => { + const w = window + if (!w.Clerk.client) { + return + } + + const signIn = await w.Clerk.client.signIn.create({ identifier: params.identifier, password: params.password }) + + if ( + signIn.status !== 'needs_second_factor' || + !signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'phone_code') + ) { + throw new Error( + `testing login's status: ${signIn.status} didn't support phone code? ${JSON.stringify(signIn.supportedSecondFactors)}`, + ) + } + await signIn.prepareSecondFactor({ strategy: 'phone_code' }) + const result = await signIn.attemptSecondFactor({ + strategy: 'phone_code', + code: '424242', + }) + + if (result.status === 'complete') { + await w.Clerk.setActive({ session: result.createdSessionId }) + } else { + reportError(`Unknown signIn status: ${result.status}`) + } +} +type TestingRole = 'researcher' | 'member' +const TestingUsers: Record = { + researcher: { + identifier: process.env.E2E_CLERK_RESEARCHER_EMAIL!, + password: process.env.E2E_CLERK_RESEARCHER_PASSWORD!, + }, + member: { + identifier: process.env.E2E_CLERK_MEMBER_EMAIL!, + password: process.env.E2E_CLERK_MEMBER_PASSWORD!, + }, +} + +type VisitClerkProtectedPageOptions = { url: string; role: TestingRole; page: Page } + +export const visitClerkProtectedPage = async ({ page, url, role }: VisitClerkProtectedPageOptions) => { await setupClerkTestingToken({ page }) await page.goto(url) + await clerkLoaded(page) + await page.evaluate(clerkSignInHelper, TestingUsers[role]) - await clerk.signIn({ - page, - signInParams: { - strategy: 'password', - identifier: process.env.E2E_CLERK_RESEARCHER_EMAIL!, - password: process.env.E2E_CLERK_RESEARCHER_PASSWORD!, - }, - }) + // the earlier page.goto likely navigated to signin + if (page.url() != url) { + await page.goto(url) + } } export const insertTestStudyData = async (opts: { memberId: string }) => { diff --git a/vitest.config.ts b/vitest.config.ts index 82c560b9..2d779f76 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { mockReset: true, - reporters: IS_CI ? ['basic', 'github-actions'] : ['verbose'], + reporters: [IS_CI ? 'github-actions' : 'verbose'], environment: 'happy-dom', setupFiles: ['tests/vitest.setup.ts'], include: ['src/**/*.(test).{js,jsx,ts,tsx}'], From d8233ca72debfcc7d0dae6a409241509393f05b5 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 10:16:06 -0600 Subject: [PATCH 05/21] extract panel --- src/components/panel.tsx | 17 +++++++ src/components/signin.tsx | 97 ++++++++++++++++++++++----------------- 2 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 src/components/panel.tsx diff --git a/src/components/panel.tsx b/src/components/panel.tsx new file mode 100644 index 00000000..7db3747f --- /dev/null +++ b/src/components/panel.tsx @@ -0,0 +1,17 @@ +import { Flex, Paper, Text } from '@mantine/core' +import React from 'react' + +export const Panel: React.FC<{ children: React.ReactNode; title: React.ReactNode }> = ({ children, title }) => { + return ( + + + + {title} + + + + {children} + + + ) +} diff --git a/src/components/signin.tsx b/src/components/signin.tsx index 0fb0fca8..b94d0fd0 100644 --- a/src/components/signin.tsx +++ b/src/components/signin.tsx @@ -2,11 +2,12 @@ import { useState } from 'react' import { isClerkApiError, reportError } from './errors' -import { Title, Anchor, Button, Group, Loader, PasswordInput, Stack, Text, TextInput, Paper } from '@mantine/core' +import { Title, Anchor, Button, Group, Loader, PasswordInput, Stack, Text, TextInput } from '@mantine/core' import { isEmail, isNotEmpty, useForm } from '@mantine/form' -import { useRouter } from 'next/navigation' -import { useSignIn } from '@clerk/nextjs' +import { Panel } from './panel' +import { useSignIn, useUser } from '@clerk/nextjs' import { SignInResource } from '@clerk/types' +import Link from 'next/link' const isUsingPhoneMFA = (signIn: SignInResource) => { return Boolean( @@ -20,9 +21,9 @@ type MFAState = false | { usingSMS: boolean; signIn: SignInResource } export function SignIn() { const { isLoaded, signIn, setActive } = useSignIn() - const [needsMFA, setNeedsMFA] = useState(false) + const { user } = useUser() - const router = useRouter() + const [needsMFA, setNeedsMFA] = useState(false) interface SignInFormValues { email: string @@ -58,7 +59,7 @@ export function SignIn() { }) if (attempt.status === 'complete') { await setActive({ session: attempt.createdSessionId }) - router.push('/') + setNeedsMFA(false) } if (attempt.status === 'needs_second_factor') { const usingSMS = isUsingPhoneMFA(attempt) @@ -88,18 +89,36 @@ export function SignIn() { if (signInAttempt.status === 'complete') { await setActive({ session: signInAttempt.createdSessionId }) - router.push('/') + setNeedsMFA(false) } else { reportError(`Unknown signIn status: ${signInAttempt.status}`) } }) + if (user) { + return ( + + {user.totpEnabled ? ( + + Your account lacks MFA protection + In order to use SafeInsights, you must have MFA enabled on your account + + Please Visit our MFA page in order to enable it. + + + ) : ( + + You have successfully signed in. Visit the <Link href="/">homepage</Link> to get started. + + )} + + ) + } + if (needsMFA) { return ( - - Enter MFA Code + Enter the code from {needsMFA.usingSMS ? 'the text message we sent' : 'your app'} -
- -
+ ) } return ( - -
- - - Welcome To SafeInsights - - - - - - - - Don't have an account? Sign Up Now - Forgot password? - - -
-
+
+ + + + + + Don't have an account? Sign Up Now + Forgot password? + + +
) } From 5aa489ce4b9912acf5933407957696ae4b6e72cf Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 12:48:22 -0600 Subject: [PATCH 06/21] refactor signup into smaller components --- src/components/links.tsx | 15 +++ src/components/signin.tsx | 165 ----------------------------- src/components/signin/complete.tsx | 28 +++++ src/components/signin/index.tsx | 28 +++++ src/components/signin/logic.ts | 10 ++ src/components/signin/mfa.tsx | 55 ++++++++++ src/components/signin/signin.tsx | 86 +++++++++++++++ 7 files changed, 222 insertions(+), 165 deletions(-) create mode 100644 src/components/links.tsx delete mode 100644 src/components/signin.tsx create mode 100644 src/components/signin/complete.tsx create mode 100644 src/components/signin/index.tsx create mode 100644 src/components/signin/logic.ts create mode 100644 src/components/signin/mfa.tsx create mode 100644 src/components/signin/signin.tsx diff --git a/src/components/links.tsx b/src/components/links.tsx new file mode 100644 index 00000000..ad4c296f --- /dev/null +++ b/src/components/links.tsx @@ -0,0 +1,15 @@ +import NextLink from 'next/link' +import { Anchor as MantineAnchor, AnchorProps } from '@mantine/core' + +export type LinkProps = AnchorProps & { + href: string + target?: string + children: React.ReactNode +} +export const Link: React.FC = ({ href, target, children, ...anchorProps }) => ( + + + {children} + + +) diff --git a/src/components/signin.tsx b/src/components/signin.tsx deleted file mode 100644 index b94d0fd0..00000000 --- a/src/components/signin.tsx +++ /dev/null @@ -1,165 +0,0 @@ -'use client' - -import { useState } from 'react' -import { isClerkApiError, reportError } from './errors' -import { Title, Anchor, Button, Group, Loader, PasswordInput, Stack, Text, TextInput } from '@mantine/core' -import { isEmail, isNotEmpty, useForm } from '@mantine/form' -import { Panel } from './panel' -import { useSignIn, useUser } from '@clerk/nextjs' -import { SignInResource } from '@clerk/types' -import Link from 'next/link' - -const isUsingPhoneMFA = (signIn: SignInResource) => { - return Boolean( - signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'phone_code') && - !signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'totp'), - ) -} - -type MFAState = false | { usingSMS: boolean; signIn: SignInResource } - -export function SignIn() { - const { isLoaded, signIn, setActive } = useSignIn() - - const { user } = useUser() - - const [needsMFA, setNeedsMFA] = useState(false) - - interface SignInFormValues { - email: string - password: string - code: string - } - - const form = useForm({ - initialValues: { - email: '', - code: '', - password: '', - }, - - validate: { - code: needsMFA ? isNotEmpty('Required') : undefined, - email: needsMFA ? undefined : isEmail('Invalid email'), - password: needsMFA ? undefined : isNotEmpty('Required'), - }, - }) - - if (!isLoaded) { - return - } - - const onSubmit = form.onSubmit(async (values) => { - if (!isLoaded) return - - try { - const attempt = await signIn.create({ - identifier: values.email, - password: values.password, - }) - if (attempt.status === 'complete') { - await setActive({ session: attempt.createdSessionId }) - setNeedsMFA(false) - } - if (attempt.status === 'needs_second_factor') { - const usingSMS = isUsingPhoneMFA(attempt) - if (usingSMS) { - await attempt.prepareSecondFactor({ strategy: 'phone_code' }) - } - setNeedsMFA({ signIn: attempt, usingSMS }) - } - } catch (err: unknown) { - reportError(err, 'failed signin') - if (isClerkApiError(err)) { - const emailError = err.errors?.find((error) => error.meta?.paramName === 'email_address') - if (emailError) { - form.setFieldError('email', emailError.longMessage) - } - } - } - }) - - const onMFA = form.onSubmit(async (values) => { - if (!isLoaded || !needsMFA) return - - const signInAttempt = await needsMFA.signIn.attemptSecondFactor({ - strategy: needsMFA.usingSMS ? 'phone_code' : 'totp', - code: values.code, - }) - - if (signInAttempt.status === 'complete') { - await setActive({ session: signInAttempt.createdSessionId }) - setNeedsMFA(false) - } else { - reportError(`Unknown signIn status: ${signInAttempt.status}`) - } - }) - - if (user) { - return ( - - {user.totpEnabled ? ( - - Your account lacks MFA protection - In order to use SafeInsights, you must have MFA enabled on your account - - Please Visit our MFA page in order to enable it. - - - ) : ( - - You have successfully signed in. Visit the <Link href="/">homepage</Link> to get started. - - )} - - ) - } - - if (needsMFA) { - return ( - - Enter the code from {needsMFA.usingSMS ? 'the text message we sent' : 'your app'} -
- - - - - - -
- ) - } - - return ( -
- - - - - - Don't have an account? Sign Up Now - Forgot password? - - -
- ) -} diff --git a/src/components/signin/complete.tsx b/src/components/signin/complete.tsx new file mode 100644 index 00000000..2f6064fb --- /dev/null +++ b/src/components/signin/complete.tsx @@ -0,0 +1,28 @@ +import { useUser } from '@clerk/nextjs' +import { Panel } from '../panel' +import { Flex, Title, Text } from '@mantine/core' +import { Link } from '../links' + +export const SigninComplete = () => { + const { user } = useUser() + + if (!user) return null + + return ( + + {user.totpEnabled ? ( + + Your account lacks MFA protection + In order to use SafeInsights, you must have MFA enabled on your account + + Please Visit our MFA page in order to enable it. + + + ) : ( + + You have successfully signed in. Visit the <Link href="/">homepage</Link> to get started. + + )} + + ) +} diff --git a/src/components/signin/index.tsx b/src/components/signin/index.tsx new file mode 100644 index 00000000..a301121b --- /dev/null +++ b/src/components/signin/index.tsx @@ -0,0 +1,28 @@ +'use client' + +import { useState } from 'react' +import { useSignIn } from '@clerk/nextjs' +import { Loader } from '@mantine/core' +import { type MFAState } from './logic' +import { SigninComplete } from './complete' +import { RequestMFA } from './mfa' +import { SignInForm } from './signin' + +export function SignIn() { + const { isLoaded } = useSignIn() + const [state, setState] = useState(false) + + if (!isLoaded) { + return + } + + const s: React.Dispatch> = setState + + return ( + <> + + + setState(false)} mfa={state} /> + + ) +} diff --git a/src/components/signin/logic.ts b/src/components/signin/logic.ts new file mode 100644 index 00000000..aa7babad --- /dev/null +++ b/src/components/signin/logic.ts @@ -0,0 +1,10 @@ +import { type SignInResource } from '@clerk/types' + +export type MFAState = false | { usingSMS: boolean; signIn: SignInResource } + +export const isUsingPhoneMFA = (signIn: SignInResource) => { + return Boolean( + signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'phone_code') && + !signIn.supportedSecondFactors?.find((sf) => sf.strategy == 'totp'), + ) +} diff --git a/src/components/signin/mfa.tsx b/src/components/signin/mfa.tsx new file mode 100644 index 00000000..98d4672b --- /dev/null +++ b/src/components/signin/mfa.tsx @@ -0,0 +1,55 @@ +import { Panel } from '../panel' +import { Flex, Text, Button, TextInput } from '@mantine/core' +import { isNotEmpty, useForm } from '@mantine/form' +import { useSignIn } from '@clerk/nextjs' + +import type { MFAState } from './logic' + +export const RequestMFA: React.FC<{ mfa: MFAState; onReset: () => void }> = ({ mfa, onReset }) => { + const { isLoaded, setActive } = useSignIn() + + const form = useForm({ + initialValues: { + code: '', + }, + + validate: { + code: isNotEmpty('Required'), + }, + }) + + if (!mfa || !isLoaded) return null + + const onMFASubmit = form.onSubmit(async (values) => { + const signInAttempt = await mfa.signIn.attemptSecondFactor({ + strategy: mfa.usingSMS ? 'phone_code' : 'totp', + code: values.code, + }) + + if (signInAttempt.status === 'complete') { + await setActive({ session: signInAttempt.createdSessionId }) + onReset() + } else { + reportError(`Unknown signIn status: ${signInAttempt.status}`) + } + }) + + return ( + + Enter the code from {mfa.usingSMS ? 'the text message we sent' : 'your app'} +
+ + + + + + +
+ ) +} diff --git a/src/components/signin/signin.tsx b/src/components/signin/signin.tsx new file mode 100644 index 00000000..f285c71f --- /dev/null +++ b/src/components/signin/signin.tsx @@ -0,0 +1,86 @@ +import { Panel } from '../panel' +import { Flex, Button, TextInput, PasswordInput } from '@mantine/core' +import { isNotEmpty, isEmail, useForm } from '@mantine/form' +import { isClerkApiError, reportError } from '../errors' +import { useSignIn, useUser } from '@clerk/nextjs' +import { Link } from '../links' + +import { type MFAState, isUsingPhoneMFA } from './logic' + +export const SignInForm: React.FC<{ + mfa: MFAState + onComplete: React.Dispatch> +}> = ({ mfa, onComplete }) => { + const { setActive, signIn } = useSignIn() + const { isSignedIn } = useUser() + + const form = useForm({ + initialValues: { + email: '', + password: '', + }, + + validate: { + email: isEmail('Invalid email'), + password: isNotEmpty('Required'), + }, + }) + + if (isSignedIn || !signIn || mfa) return null + + const onSubmit = form.onSubmit(async (values) => { + try { + const attempt = await signIn.create({ + identifier: values.email, + password: values.password, + }) + if (attempt.status === 'complete') { + await setActive({ session: attempt.createdSessionId }) + onComplete(false) + } + if (attempt.status === 'needs_second_factor') { + const usingSMS = isUsingPhoneMFA(attempt) + if (usingSMS) { + await attempt.prepareSecondFactor({ strategy: 'phone_code' }) + } + onComplete({ signIn: attempt, usingSMS }) + } + } catch (err: unknown) { + reportError(err, 'failed signin') + if (isClerkApiError(err)) { + const emailError = err.errors?.find((error) => error.meta?.paramName === 'email_address') + if (emailError) { + form.setFieldError('email', emailError.longMessage) + } + } + } + }) + + return ( +
+ + + + + + Don't have an account? Sign Up Now + Forgot password? + + +
+ ) +} From ae9294cb0aab73de6a40e57071698c4d61a4103b Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 12:48:44 -0600 Subject: [PATCH 07/21] add signin spec --- playwright.config.ts | 47 +++++++++++++++++++++++--------------------- tests/e2e.helpers.ts | 13 +++++++++--- tests/signin.spec.ts | 27 +++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 25 deletions(-) create mode 100644 tests/signin.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 6627b027..adcbe115 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,30 +6,33 @@ const reporters: ReporterDescription[] = [] if (process.argv.includes('--ui')) { reporters.push(['list']) } else { - reporters.push([ - 'monocart-reporter', - { - outputFile: path.resolve('./tmp/test-results/e2e/coverage.html'), - coverage: { - outputDir: path.resolve('./tmp/code-coverage/e2e'), - entryFilter: (entry: { url: string; source: string }) => { - return entry.url.match(/\/chunks\/src/) && !entry.source.match(/TURBOPACK_CHUNK_LISTS/) - }, - sourceFilter: testsCoverageSourceFilter, - reports: [ - 'raw', - 'v8', - 'console-summary', - [ - 'lcovonly', - { - file: 'lcov/code-coverage.lcov.info', - }, + reporters.push( + [ + 'monocart-reporter', + { + outputFile: path.resolve('./tmp/test-results/e2e/coverage.html'), + coverage: { + outputDir: path.resolve('./tmp/code-coverage/e2e'), + entryFilter: (entry: { url: string; source: string }) => { + return entry.url.match(/\/chunks\/src/) && !entry.source.match(/TURBOPACK_CHUNK_LISTS/) + }, + sourceFilter: testsCoverageSourceFilter, + reports: [ + 'raw', + 'v8', + 'console-summary', + [ + 'lcovonly', + { + file: 'lcov/code-coverage.lcov.info', + }, + ], ], - ], + }, }, - }, - ]) + ], + ['list'], + ) } export default defineConfig({ diff --git a/tests/e2e.helpers.ts b/tests/e2e.helpers.ts index 0a1e1098..e9b07d47 100644 --- a/tests/e2e.helpers.ts +++ b/tests/e2e.helpers.ts @@ -4,6 +4,7 @@ import { setupClerkTestingToken } from '@clerk/testing/playwright' import fs from 'fs' import path from 'path' +export { clerk } from '@clerk/testing/playwright' export * from './common.helpers' export { fs, path } @@ -100,8 +101,11 @@ const clerkLoaded = async (page: Page) => { type ClerkSignInParams = { password: string identifier: string + mfa: string } +export const CLERK_MFA_CODE = '424242' + const clerkSignInHelper = async (params: ClerkSignInParams) => { const w = window if (!w.Clerk.client) { @@ -121,7 +125,7 @@ const clerkSignInHelper = async (params: ClerkSignInParams) => { await signIn.prepareSecondFactor({ strategy: 'phone_code' }) const result = await signIn.attemptSecondFactor({ strategy: 'phone_code', - code: '424242', + code: params.mfa, }) if (result.status === 'complete') { @@ -130,13 +134,16 @@ const clerkSignInHelper = async (params: ClerkSignInParams) => { reportError(`Unknown signIn status: ${result.status}`) } } -type TestingRole = 'researcher' | 'member' -const TestingUsers: Record = { + +export type TestingRole = 'researcher' | 'member' +export const TestingUsers: Record = { researcher: { + mfa: CLERK_MFA_CODE, identifier: process.env.E2E_CLERK_RESEARCHER_EMAIL!, password: process.env.E2E_CLERK_RESEARCHER_PASSWORD!, }, member: { + mfa: CLERK_MFA_CODE, identifier: process.env.E2E_CLERK_MEMBER_EMAIL!, password: process.env.E2E_CLERK_MEMBER_PASSWORD!, }, diff --git a/tests/signin.spec.ts b/tests/signin.spec.ts new file mode 100644 index 00000000..cbb367a3 --- /dev/null +++ b/tests/signin.spec.ts @@ -0,0 +1,27 @@ +import { clerk, test, TestingUsers, CLERK_MFA_CODE } from './e2e.helpers' + +test.describe('user sign in', async () => { + for (const [role, props] of Object.entries(TestingUsers)) { + test(`login as ${role}`, async ({ page }) => { + await page.goto('/account/signin') + await clerk.signOut({ page }) // probably not needed + + const fillForm = async () => { + await page.getByLabel('email').fill(props.identifier) + await page.getByLabel('password').fill(props.password) + await page.getByRole('button', { name: 'login' }).click() + } + + await fillForm() + + await page.getByRole('button', { name: 'reenter' }).click() + + await fillForm() + + await page.getByLabel('code').fill(CLERK_MFA_CODE) + await page.getByRole('button', { name: 'login' }).click() + + await page.waitForSelector('text=success') + }) + } +}) From 9a56f5c6bac35b2be412a7650492182fb770251a Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 16:16:51 -0600 Subject: [PATCH 08/21] page can't export extra components --- src/app/account/mfa/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/account/mfa/page.tsx b/src/app/account/mfa/page.tsx index 2850b656..c7088ab8 100644 --- a/src/app/account/mfa/page.tsx +++ b/src/app/account/mfa/page.tsx @@ -21,7 +21,7 @@ const HasMFA = () => { } // Generate and display backup codes -export function GenerateBackupCodes() { +function GenerateBackupCodes() { const { user } = useUser() const [backupCodes, setBackupCodes] = React.useState(undefined) From ecb1a660ea12ecd16001d359f16a2eb3e9d36534 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 16:21:35 -0600 Subject: [PATCH 09/21] simplify logic, single filter --- src/app/account/mfa/sms/page.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/account/mfa/sms/page.tsx b/src/app/account/mfa/sms/page.tsx index da943070..4f3ef145 100644 --- a/src/app/account/mfa/sms/page.tsx +++ b/src/app/account/mfa/sms/page.tsx @@ -68,11 +68,6 @@ const ManageAvailablePhoneNumbers = () => { if (!user) return null - // Check if any phone numbers aren't reserved for MFA - const availableForMfaPhones = user.phoneNumbers - .filter((ph) => ph.verification.status === 'verified') - .filter((ph) => !ph.reservedForSecondFactor) - // Reserve a phone number for MFA const reservePhoneForMfa = async (phone: PhoneNumberResource) => { // Set the phone number as reserved for MFA @@ -81,7 +76,10 @@ const ManageAvailablePhoneNumbers = () => { await user.reload() } - if (availableForMfaPhones.length === 0) { + // phone numbers are valid for MFA but aren't used for it + const availableForMfaPhones = user.phoneNumbers.filter(phone => phone.verification.status === 'verified' && !phone.reservedForSecondFactor) + + if (availableForMfaPhones.length) { return

There are currently no verified phone numbers available to be reserved for MFA.

} From 688b23f571fd3f8620c21de19b1f29a2d37209be Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 17:38:19 -0600 Subject: [PATCH 10/21] cleanup mfa pages a bit, add simple e2e tests --- src/app/account/mfa/app/page.tsx | 58 +++++------- src/app/account/mfa/backup-codes.tsx | 46 ++++++++++ src/app/account/mfa/page.tsx | 132 +++++++++------------------ src/app/account/mfa/sms/page.tsx | 4 +- src/app/account/signin/page.tsx | 2 +- src/components/links.tsx | 15 ++- src/components/panel.tsx | 11 ++- src/components/signin/complete.tsx | 10 +- src/components/signin/mfa.tsx | 4 +- src/middleware.ts | 4 +- tests/e2e.helpers.ts | 8 +- tests/mfa.spec.ts | 32 +++++++ 12 files changed, 191 insertions(+), 135 deletions(-) create mode 100644 src/app/account/mfa/backup-codes.tsx create mode 100644 tests/mfa.spec.ts diff --git a/src/app/account/mfa/app/page.tsx b/src/app/account/mfa/app/page.tsx index 59012f5f..4130d19f 100644 --- a/src/app/account/mfa/app/page.tsx +++ b/src/app/account/mfa/app/page.tsx @@ -2,13 +2,14 @@ import { useUser } from '@clerk/nextjs' import { TOTPResource } from '@clerk/types' -import Link from 'next/link' import * as React from 'react' import { QRCodeSVG } from 'qrcode.react' -import { GenerateBackupCodes } from '../page' +import { GenerateBackupCodes } from '../backup-codes' import { useForm } from '@mantine/form' -import { Button, TextInput, Title, Flex } from '@mantine/core' +import { Button, TextInput, Text, Flex, Container } from '@mantine/core' import { errorToString, reportError } from '@/components/errors' +import { Panel } from '@/components/panel' +import { ButtonLink } from '@/components/links' type AddTotpSteps = 'add' | 'verify' | 'backupcodes' | 'success' @@ -29,11 +30,7 @@ function AddTotpScreen({ setStep }: { setStep: React.Dispatch - - Add MFA - - + {totp && displayFormat === 'qr' && ( <> @@ -57,7 +54,7 @@ function AddTotpScreen({ setStep }: { setStep: React.Dispatch

Once you have set up your authentication app, verify your code

- +
) } @@ -84,8 +81,7 @@ function VerifyTotpScreen({ setStep }: { setStep: React.Dispatch - Verify MFA code +
+ - - +
) } function BackupCodeScreen({ setStep }: { setStep: React.Dispatch> }) { return ( - <> -

Verification was a success!

-
-

- Save this list of backup codes somewhere safe in case you need to access your account in an - emergency -

- - -
- + + + Save this list of backup codes somewhere safe in case you need to access your account in an emergency + + + + ) } function SuccessScreen() { return ( - <> -

Success!

-

You have successfully added TOTP MFA via an authentication application.

- - - - + + You have successfully added TOTP MFA with an authentication application. + + Return to homepage + ) } @@ -144,11 +136,11 @@ export default function AddMFaScreen() { } return ( - <> + {step === 'add' && } {step === 'verify' && } {step === 'backupcodes' && } {step === 'success' && } - + ) } diff --git a/src/app/account/mfa/backup-codes.tsx b/src/app/account/mfa/backup-codes.tsx new file mode 100644 index 00000000..fb72b1b6 --- /dev/null +++ b/src/app/account/mfa/backup-codes.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { BackupCodeResource } from '@clerk/types' +import { reportError } from '@/components/errors' +import { useUser } from '@clerk/nextjs' + +// Generate and display backup codes +export function GenerateBackupCodes() { + const { user } = useUser() + const [backupCodes, setBackupCodes] = React.useState(undefined) + + const [loading, setLoading] = React.useState(false) + + React.useEffect(() => { + if (backupCodes) { + return + } + + setLoading(true) + void user + ?.createBackupCode() + .then((backupCode: BackupCodeResource) => { + setBackupCodes(backupCode) + setLoading(false) + }) + .catch((err) => { + reportError(err, 'Failed to generate backup codes') + setLoading(false) + }) + }, [backupCodes, user]) + + if (loading) { + return

Loading...

+ } + + if (!backupCodes) { + return

There was a problem generating backup codes

+ } + + return ( +
    + {backupCodes.codes.map((code, index) => ( +
  1. {code}
  2. + ))} +
+ ) +} diff --git a/src/app/account/mfa/page.tsx b/src/app/account/mfa/page.tsx index c7088ab8..c0e3b502 100644 --- a/src/app/account/mfa/page.tsx +++ b/src/app/account/mfa/page.tsx @@ -2,63 +2,21 @@ import * as React from 'react' import { useClerk, useUser } from '@clerk/nextjs' -import Link from 'next/link' -import { Button, Flex, Title } from '@mantine/core' -import { BackupCodeResource } from '@clerk/types' -import { reportError } from '@/components/errors' +import { Link } from '@/components/links' +import { Container, Button, Flex, Text, Title } from '@mantine/core' +import { GenerateBackupCodes } from './backup-codes' +import { Panel } from '@/components/panel' const HasMFA = () => { return ( -
-

- You have successfully enabled MFA on your account - - + + + You have successfully enabled MFA on your account + + Return to homepage -

-
- ) -} - -// Generate and display backup codes -function GenerateBackupCodes() { - const { user } = useUser() - const [backupCodes, setBackupCodes] = React.useState(undefined) - - const [loading, setLoading] = React.useState(false) - - React.useEffect(() => { - if (backupCodes) { - return - } - - setLoading(true) - void user - ?.createBackupCode() - .then((backupCode: BackupCodeResource) => { - setBackupCodes(backupCode) - setLoading(false) - }) - .catch((err) => { - reportError(err, 'Failed to generate backup codes') - setLoading(false) - }) - }, [backupCodes, user]) - - if (loading) { - return

Loading...

- } - - if (!backupCodes) { - return

There was a problem generating backup codes

- } - - return ( -
    - {backupCodes.codes.map((code, index) => ( -
  1. {code}
  2. - ))} -
+ + ) } @@ -73,45 +31,45 @@ export default function ManageMFA() { return

You must be logged in to access this page

} - if (user.totpEnabled) return + if (user.twoFactorEnabled && !window.location.search.includes('TESTING_FORCE_NO_MFA')) return return ( - <> - MFA is required + + + In order to use SafeInsights, your account must have MFA enabled - In order to use SafeInsights, your account must have MFA enabled - - - - - - - {user.phoneNumbers.length ? ( - - + + + - ) : ( - -

You could use SMS MFA if you have a phone number entered on your account.

- -
- )} -
- {/* Manage backup codes */} - {user.backupCodeEnabled && user.twoFactorEnabled && ( -
-

+ {user.phoneNumbers.length ? ( + + + + ) : ( + +

You could use SMS MFA if you have a phone number entered on your account.

+ + + )} + + + {/* Manage backup codes */} + {user.backupCodeEnabled && user.twoFactorEnabled && ( + Generate new backup codes? - -

-
- )} - {showNewCodes && ( - <> - - - - )} - + + )} + {showNewCodes && ( + <> + + + + )} +
+
) } diff --git a/src/app/account/mfa/sms/page.tsx b/src/app/account/mfa/sms/page.tsx index 4f3ef145..4192c919 100644 --- a/src/app/account/mfa/sms/page.tsx +++ b/src/app/account/mfa/sms/page.tsx @@ -77,7 +77,9 @@ const ManageAvailablePhoneNumbers = () => { } // phone numbers are valid for MFA but aren't used for it - const availableForMfaPhones = user.phoneNumbers.filter(phone => phone.verification.status === 'verified' && !phone.reservedForSecondFactor) + const availableForMfaPhones = user.phoneNumbers.filter( + (phone) => phone.verification.status === 'verified' && !phone.reservedForSecondFactor, + ) if (availableForMfaPhones.length) { return

There are currently no verified phone numbers available to be reserved for MFA.

diff --git a/src/app/account/signin/page.tsx b/src/app/account/signin/page.tsx index 062bac1d..619c9d95 100644 --- a/src/app/account/signin/page.tsx +++ b/src/app/account/signin/page.tsx @@ -1,4 +1,4 @@ -import { SignIn } from '@/components/signin' +import { SignIn } from '@/components/signin/index' import { pageStyles } from '@/styles/common' import { Container } from '@/styles/generated/jsx' diff --git a/src/components/links.tsx b/src/components/links.tsx index ad4c296f..f5c1ef5b 100644 --- a/src/components/links.tsx +++ b/src/components/links.tsx @@ -1,11 +1,12 @@ import NextLink from 'next/link' -import { Anchor as MantineAnchor, AnchorProps } from '@mantine/core' +import { Anchor as MantineAnchor, AnchorProps, Button, ButtonProps } from '@mantine/core' export type LinkProps = AnchorProps & { href: string target?: string children: React.ReactNode } + export const Link: React.FC = ({ href, target, children, ...anchorProps }) => ( @@ -13,3 +14,15 @@ export const Link: React.FC = ({ href, target, children, ...anchorPro ) + +export type ButtonLinkProps = ButtonProps & { + href: string + target?: string + children: React.ReactNode +} + +export const ButtonLink: React.FC = ({ href, target, children, ...anchorProps }) => ( + +) diff --git a/src/components/panel.tsx b/src/components/panel.tsx index 7db3747f..0d42f4f6 100644 --- a/src/components/panel.tsx +++ b/src/components/panel.tsx @@ -1,9 +1,14 @@ -import { Flex, Paper, Text } from '@mantine/core' +import { Flex, FlexProps, Paper, Text } from '@mantine/core' import React from 'react' -export const Panel: React.FC<{ children: React.ReactNode; title: React.ReactNode }> = ({ children, title }) => { +export type PanelProps = FlexProps & { + title: string + children: React.ReactNode +} + +export const Panel: React.FC = ({ children, title, ...flexProps }) => { return ( - + {title} diff --git a/src/components/signin/complete.tsx b/src/components/signin/complete.tsx index 2f6064fb..ff6e584c 100644 --- a/src/components/signin/complete.tsx +++ b/src/components/signin/complete.tsx @@ -10,7 +10,11 @@ export const SigninComplete = () => { return ( - {user.totpEnabled ? ( + {user.twoFactorEnabled ? ( + + You have successfully signed in. Visit the <Link href="/">homepage</Link> to get started. + + ) : ( Your account lacks MFA protection In order to use SafeInsights, you must have MFA enabled on your account @@ -18,10 +22,6 @@ export const SigninComplete = () => { Please Visit our MFA page in order to enable it. - ) : ( - - You have successfully signed in. Visit the <Link href="/">homepage</Link> to get started. - )} ) diff --git a/src/components/signin/mfa.tsx b/src/components/signin/mfa.tsx index 98d4672b..4b40e851 100644 --- a/src/components/signin/mfa.tsx +++ b/src/components/signin/mfa.tsx @@ -46,7 +46,9 @@ export const RequestMFA: React.FC<{ mfa: MFAState; onReset: () => void }> = ({ m {...form.getInputProps('code')} /> - + diff --git a/src/middleware.ts b/src/middleware.ts index 5c642e96..6690556c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -38,10 +38,10 @@ const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) const OPENSTAX_ORG_SLUG = 'openstax' const SAFEINSIGHTS_ORG_SLUG = 'safe-insights' -const ANON_ROUTES: Array = ['/account/reset-password', '/account/signup', '/account/signin'] - const MFA_ROUTE = '/account/mfa' +const ANON_ROUTES: Array = ['/account/reset-password', '/account/signup', '/account/signin'] + // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware diff --git a/tests/e2e.helpers.ts b/tests/e2e.helpers.ts index e9b07d47..9a1e16a3 100644 --- a/tests/e2e.helpers.ts +++ b/tests/e2e.helpers.ts @@ -153,7 +153,13 @@ type VisitClerkProtectedPageOptions = { url: string; role: TestingRole; page: Pa export const visitClerkProtectedPage = async ({ page, url, role }: VisitClerkProtectedPageOptions) => { await setupClerkTestingToken({ page }) - await page.goto(url) + await page.goto('/account/signin') + + await clerkLoaded(page) + await page.evaluate(() => { + window.Clerk.session?.end() + }) + await page.goto('/account/signin') await clerkLoaded(page) await page.evaluate(clerkSignInHelper, TestingUsers[role]) diff --git a/tests/mfa.spec.ts b/tests/mfa.spec.ts new file mode 100644 index 00000000..8b3b48ca --- /dev/null +++ b/tests/mfa.spec.ts @@ -0,0 +1,32 @@ +import { visitClerkProtectedPage, clerk, test, TestingUsers, CLERK_MFA_CODE } from './e2e.helpers' + +test.describe('MFA authentication', async () => { + test('adds using app', async ({ page }) => { + await visitClerkProtectedPage({ page, url: '/account/mfa?TESTING_FORCE_NO_MFA=1', role: 'member' }) + + await page.getByRole('button', { name: 'authenticator' }).click() + + await page.getByRole('button', { name: 'verify' }).click() + + await page.getByLabel('code').fill('123456') + + await page.getByRole('button', { name: 'verify' }).click() + + await page.waitForSelector(`text=incorrect`) + + await page.getByRole('button', { name: 'retry' }).click() + }) + + test('adds using sms', async ({ page }) => { + await visitClerkProtectedPage({ page, url: '/account/mfa?TESTING_FORCE_NO_MFA=1', role: 'member' }) + await page.getByRole('button', { name: 'sms' }).click() + + await page.getByRole('button', { name: 'user profile' }).click() + + await page.waitForSelector('text=add phone number') + + await page.getByLabel('close modal').click() + + await page.getByRole('button', { name: 'homepage' }).click() + }) +}) From 3a1edca0cad923935ff6579ff723e6f38737dd30 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 18:27:09 -0600 Subject: [PATCH 11/21] overrides was unused --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index c6986945..52087394 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,5 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "*", "vitest-monocart-coverage": "^3.0.0" - }, - "overrides": {} + } } From 1a766a9bf09f00397cfbdf329e9e925ffda2e8f0 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 19:14:10 -0600 Subject: [PATCH 12/21] add MFA to label, helps prompt 1password --- src/components/signin/mfa.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/signin/mfa.tsx b/src/components/signin/mfa.tsx index 4b40e851..d347e62f 100644 --- a/src/components/signin/mfa.tsx +++ b/src/components/signin/mfa.tsx @@ -40,7 +40,7 @@ export const RequestMFA: React.FC<{ mfa: MFAState; onReset: () => void }> = ({ m
Date: Wed, 12 Feb 2025 19:14:40 -0600 Subject: [PATCH 13/21] report coverage difference --- .github/workflows/checks.yml | 7 ++++--- tests/merged-coverage.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 987c65ab..a7b1a1c4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -40,10 +40,11 @@ jobs: run: | cat tmp/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY cat tmp/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY - - name: Add comment to PR - uses: mshick/add-pr-comment@v2 + - name: Coverage Diff + uses: greatwizard/coverage-diff-action@v1 with: - message-path: tmp/code-coverage/merged/coverage-summary.md + github-token: ${{ secrets.GITHUB_TOKEN }} + coverage-filename: tmp/code-coverage/merged/coverage-summary.json - name: Test build run: | echo NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} > .env diff --git a/tests/merged-coverage.ts b/tests/merged-coverage.ts index b17df66c..27316a92 100644 --- a/tests/merged-coverage.ts +++ b/tests/merged-coverage.ts @@ -7,7 +7,7 @@ const coverageOptions: CoverageReportOptions = { outputDir: './tmp/code-coverage/merged', sourceFilter: testsCoverageSourceFilter, clean: true, - reports: ['markdown-summary', 'markdown-details', 'console-details', 'html'], + reports: ['markdown-summary', 'markdown-details', 'console-details', 'json-summary', 'html'], } await new CoverageReport(coverageOptions).generate() From 4d38ea07fa0cbb83f8ba87ede2069223f46d15c8 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 19:41:54 -0600 Subject: [PATCH 14/21] space links --- src/components/signin/signin.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/signin/signin.tsx b/src/components/signin/signin.tsx index f285c71f..7947b6d6 100644 --- a/src/components/signin/signin.tsx +++ b/src/components/signin/signin.tsx @@ -75,10 +75,12 @@ export const SignInForm: React.FC<{ placeholder="Password" aria-label="Password" /> - + - Don't have an account? Sign Up Now - Forgot password? + + Don't have an account? Sign Up Now + Forgot password? + From 2f800dce8bcb9caa65389f82fe43a0fb28a6e156 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 20:11:15 -0600 Subject: [PATCH 15/21] use fork --- .github/workflows/checks.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a7b1a1c4..9a3d35cc 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -41,7 +41,8 @@ jobs: cat tmp/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY cat tmp/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY - name: Coverage Diff - uses: greatwizard/coverage-diff-action@v1 + uses: bultkrantz/coverage-diff-action@v5 + if: always() with: github-token: ${{ secrets.GITHUB_TOKEN }} coverage-filename: tmp/code-coverage/merged/coverage-summary.json From d4e5d3ae38d4a41a0ccf3d7f915147a8e5211dd9 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 20:19:08 -0600 Subject: [PATCH 16/21] test w/out cov --- .github/workflows/checks.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9a3d35cc..caa4c918 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -40,12 +40,6 @@ jobs: run: | cat tmp/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY cat tmp/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY - - name: Coverage Diff - uses: bultkrantz/coverage-diff-action@v5 - if: always() - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - coverage-filename: tmp/code-coverage/merged/coverage-summary.json - name: Test build run: | echo NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} > .env From cd149329b7daafbc146ce2d6899d01ff1ecf18e4 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 20:20:00 -0600 Subject: [PATCH 17/21] restore --- .github/workflows/checks.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index caa4c918..9a3d35cc 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -40,6 +40,12 @@ jobs: run: | cat tmp/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY cat tmp/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY + - name: Coverage Diff + uses: bultkrantz/coverage-diff-action@v5 + if: always() + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + coverage-filename: tmp/code-coverage/merged/coverage-summary.json - name: Test build run: | echo NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} > .env From de6d09fef81f8bd67386e8e68dc2a22a9b874006 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 20:22:02 -0600 Subject: [PATCH 18/21] version --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9a3d35cc..b7a5d0ef 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -41,7 +41,7 @@ jobs: cat tmp/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY cat tmp/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY - name: Coverage Diff - uses: bultkrantz/coverage-diff-action@v5 + uses: bultkrantz/coverage-diff-action@v5.0.1 if: always() with: github-token: ${{ secrets.GITHUB_TOKEN }} From 13ea525f872b54705a6cd1952141f343df570793 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 20:31:55 -0600 Subject: [PATCH 19/21] add base-summary --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b7a5d0ef..25af78f6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -45,6 +45,7 @@ jobs: if: always() with: github-token: ${{ secrets.GITHUB_TOKEN }} + base-summary-filename: base-summary.json coverage-filename: tmp/code-coverage/merged/coverage-summary.json - name: Test build run: | From 8e94ebd74d1128042e2a8107cf339f1e4cb07af6 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 12 Feb 2025 20:52:00 -0600 Subject: [PATCH 20/21] move coverage files from tmp to tests/coverage --- .github/workflows/checks.yml | 6 +++--- .gitignore | 4 ++-- playwright.config.ts | 4 ++-- tests/coverage/.gitkeep | 0 tests/merged-coverage.ts | 4 ++-- vitest.config.ts | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 tests/coverage/.gitkeep diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 25af78f6..e0de7adc 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -38,15 +38,15 @@ jobs: run: docker compose exec mgmnt-app npm run ci - name: Add code coverage to actions summary run: | - cat tmp/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY - cat tmp/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY + cat tests/coverage/code-coverage/merged/coverage-summary.md >> $GITHUB_STEP_SUMMARY + cat tests/coverage/code-coverage/merged/coverage-details.md >> $GITHUB_STEP_SUMMARY - name: Coverage Diff uses: bultkrantz/coverage-diff-action@v5.0.1 if: always() with: github-token: ${{ secrets.GITHUB_TOKEN }} base-summary-filename: base-summary.json - coverage-filename: tmp/code-coverage/merged/coverage-summary.json + coverage-filename: tests/coverage/code-coverage/merged/coverage-summary.json - name: Test build run: | echo NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} > .env diff --git a/.gitignore b/.gitignore index 72695fac..85d91ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ yarn-error.log* .npm .eslintcache package-lock.json -coverage/ +tests/coverage/ playwright-report/ src/styles/generated -./tests/fixtures/temp/large-file \ No newline at end of file +./tests/fixtures/temp/large-file diff --git a/playwright.config.ts b/playwright.config.ts index adcbe115..207158a3 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,9 +10,9 @@ if (process.argv.includes('--ui')) { [ 'monocart-reporter', { - outputFile: path.resolve('./tmp/test-results/e2e/coverage.html'), + outputFile: path.resolve('./tests/coverage/test-results/e2e/coverage.html'), coverage: { - outputDir: path.resolve('./tmp/code-coverage/e2e'), + outputDir: path.resolve('./tests/coverage/code-coverage/e2e'), entryFilter: (entry: { url: string; source: string }) => { return entry.url.match(/\/chunks\/src/) && !entry.source.match(/TURBOPACK_CHUNK_LISTS/) }, diff --git a/tests/coverage/.gitkeep b/tests/coverage/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/merged-coverage.ts b/tests/merged-coverage.ts index 27316a92..fbf0d69d 100644 --- a/tests/merged-coverage.ts +++ b/tests/merged-coverage.ts @@ -3,8 +3,8 @@ import { testsCoverageSourceFilter } from './coverage.mjs' const coverageOptions: CoverageReportOptions = { name: 'Coverage Report', - inputDir: ['./tmp/code-coverage/unit/raw', './tmp/code-coverage/e2e/raw'], - outputDir: './tmp/code-coverage/merged', + inputDir: ['./tests/coverage/code-coverage/unit/raw', './tests/coverage/code-coverage/e2e/raw'], + outputDir: './tests/coverage/code-coverage/merged', sourceFilter: testsCoverageSourceFilter, clean: true, reports: ['markdown-summary', 'markdown-details', 'console-details', 'json-summary', 'html'], diff --git a/vitest.config.ts b/vitest.config.ts index 2d779f76..87f1744c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,12 +16,12 @@ export default defineConfig({ include: ['src/**/*.(test).{js,jsx,ts,tsx}'], coverage: { enabled: IS_CI, - reportsDirectory: 'tmp/code-coverage/unit', + reportsDirectory: 'tests/coverage/code-coverage/unit', clean: true, coverageReportOptions: { reports: ['raw', 'console-details', 'v8', 'html'], lcov: true, - outputDir: 'tmp/code-coverage/unit', + outputDir: 'tests/coverage/code-coverage/unit', clean: true, sourceFilter: testsCoverageSourceFilter, }, From ba3185df1ea51acd630a2d8e664e598699095db4 Mon Sep 17 00:00:00 2001 From: Chris Bendel Date: Thu, 13 Feb 2025 10:50:26 -0500 Subject: [PATCH 21/21] Fix bad typing --- tests/e2e.helpers.ts | 3 --- tests/member-review-study.spec.tsx | 6 +++--- tests/researcher-create-study.spec.tsx | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/e2e.helpers.ts b/tests/e2e.helpers.ts index dc35de50..91787adc 100644 --- a/tests/e2e.helpers.ts +++ b/tests/e2e.helpers.ts @@ -11,9 +11,6 @@ export { fs, path } // since we're extending test from here, we might as well export some other often-used items export { expect, type Page } from '@playwright/test' -export const USE_COVERAGE = process.argv.includes('--coverage') -//import { useSignIn } from '@clerk/nextjs' - export type CollectV8CodeCoverageOptions = { browserType: BrowserType page: Page diff --git a/tests/member-review-study.spec.tsx b/tests/member-review-study.spec.tsx index 71985cb6..77db696b 100644 --- a/tests/member-review-study.spec.tsx +++ b/tests/member-review-study.spec.tsx @@ -1,4 +1,4 @@ -import { visitClerkProtectedPage, test, expect, Role } from './e2e.helpers' +import { visitClerkProtectedPage, test, expect } from './e2e.helpers' test.describe('BMA member review', () => { const studyTitle = `E2E Member review - ${[...Array(6)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')}` @@ -8,7 +8,7 @@ test.describe('BMA member review', () => { const codeLine = 'print("Hello, Tester")' test.beforeEach(async ({ page }) => { - await visitClerkProtectedPage({ page, role: Role.Researcher, url: '/' }) + await visitClerkProtectedPage({ page, role: 'researcher', url: '/' }) await expect(page).toHaveTitle(/SafeInsights/) await page.getByRole('button', { name: /propose/i }).click() await page.getByLabel(/title/i).fill(studyTitle) @@ -29,7 +29,7 @@ test.describe('BMA member review', () => { }) test('member reviews a study main code file', async ({ page }) => { - await visitClerkProtectedPage({ page, role: Role.Member, url: '/' }) + await visitClerkProtectedPage({ page, role: 'member', url: '/' }) await page.getByRole('button', { name: /review studies/i }).click() await page.locator('li').filter({ hasText: studyTitle }).getByRole('link').click() await page.getByRole('button', { name: /researcher code/i }).click() diff --git a/tests/researcher-create-study.spec.tsx b/tests/researcher-create-study.spec.tsx index d3b0fd86..ace7dda2 100644 --- a/tests/researcher-create-study.spec.tsx +++ b/tests/researcher-create-study.spec.tsx @@ -1,10 +1,10 @@ -import { visitClerkProtectedPage, test, expect, Role } from './e2e.helpers' +import { visitClerkProtectedPage, test, expect } from './e2e.helpers' test.describe('app', () => { const testTitle = 'A E2E Test Study' test.beforeEach('researcher creates a study', async ({ page }) => { - await visitClerkProtectedPage({ page, role: Role.Researcher, url: '/' }) + await visitClerkProtectedPage({ page, role: 'researcher', url: '/' }) await expect(page).toHaveTitle(/SafeInsights/)