diff --git a/eslint.config.js b/eslint.config.js
index 092408a9..c17615ca 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -3,7 +3,6 @@ import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
-
export default tseslint.config(
{ ignores: ['dist'] },
{
@@ -19,10 +18,11 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
- ],
+ // TODO[cfviotti]: Check if removing this rule is the right path to mitigate these warnings.
+ // 'react-refresh/only-export-components': [
+ // 'warn',
+ // { allowConstantExport: true },
+ // ],
},
},
)
diff --git a/package-lock.json b/package-lock.json
index 152866f4..4fa93f73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,7 +31,10 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.6",
"@radix-ui/react-visually-hidden": "^1.1.0",
+ "@react-oauth/google": "^0.12.1",
+ "@stepperize/react": "^5.1.5",
"@tanstack/react-query": "^5.59.15",
+ "@tanstack/react-query-devtools": "^5.74.6",
"@tanstack/react-table": "^8.20.5",
"@uiw/react-textarea-code-editor": "^3.1.0",
"@xyflow/react": "^12.3.6",
@@ -71,7 +74,6 @@
"@storybook/react-vite": "^8.6.9",
"@storybook/testing-library": "^0.2.1",
"@storybook/types": "^8.6.9",
- "@tanstack/react-query-devtools": "^5.74.4",
"@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
@@ -87,6 +89,7 @@
"autoprefixer": "^10.4.20",
"chromatic": "^11.27.0",
"eslint": "^9.9.0",
+ "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
@@ -3494,6 +3497,16 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
},
+ "node_modules/@react-oauth/google": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz",
+ "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
@@ -3763,6 +3776,34 @@
"win32"
]
},
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@stepperize/core": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@stepperize/core/-/core-1.2.5.tgz",
+ "integrity": "sha512-2jk/yG4aZla+i67zcsXrTfzBbBx/ILtuaafsBpwZCnfVhPcvoKAPQvSm55p0Q6nQPcRVJ3nKexcR8/k5V+XQBA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5.0.2"
+ }
+ },
+ "node_modules/@stepperize/react": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/@stepperize/react/-/react-5.1.5.tgz",
+ "integrity": "sha512-2g3rWY9j4aViH3ppVHxnGHhSOy3TSUlj/F0h9bG6PKcrmmes85mMHMB1lMjA7vUtkdedczZSn90ybrwv4LT8lw==",
+ "license": "MIT",
+ "dependencies": {
+ "@stepperize/core": "1.2.5"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@storybook/addon-actions": {
"version": "8.6.9",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.6.9.tgz",
@@ -4864,10 +4905,9 @@
}
},
"node_modules/@tanstack/query-devtools": {
- "version": "5.73.3",
- "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.73.3.tgz",
- "integrity": "sha512-hBQyYwsOuO7QOprK75NzfrWs/EQYjgFA0yykmcvsV62q0t6Ua97CU3sYgjHx0ZvxkXSOMkY24VRJ5uv9f5Ik4w==",
- "dev": true,
+ "version": "5.74.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.6.tgz",
+ "integrity": "sha512-djaFT11mVCOW3e0Ezfyiq7T6OoHy2LRI1fUFQvj+G6+/4A1FkuRMNUhQkdP1GXlx8id0f1/zd5fgDpIy5SU/Iw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -4891,13 +4931,12 @@
}
},
"node_modules/@tanstack/react-query-devtools": {
- "version": "5.74.4",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.4.tgz",
- "integrity": "sha512-PGCAcytQMmeagoeGG45ccBhrC1x0/5OlNjsM1FAb9OfsQZIhPzjwjhGcwmMu6TbT4RIHgvjxLwC5NHgkUwJQzw==",
- "dev": true,
+ "version": "5.74.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.74.6.tgz",
+ "integrity": "sha512-vlsDwz4/FsblK0h7VAlXUdJ+9OV+i1n8OLb8CLLAZqu0M9GCnbajytZwsRmns33PXBZ6wQBJ859kg6aajx+e9Q==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-devtools": "5.73.3"
+ "@tanstack/query-devtools": "5.74.6"
},
"funding": {
"type": "github",
@@ -5224,6 +5263,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
@@ -5838,6 +5884,109 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array-includes": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+ "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -5862,6 +6011,16 @@
"node": ">=4"
}
},
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -6715,6 +6874,60 @@
"node": ">=12"
}
},
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
@@ -7033,6 +7246,72 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-abstract": {
+ "version": "1.23.9",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
+ "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.0",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-regex": "^1.2.1",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.0",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.3",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.18"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -7084,6 +7363,53 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -7221,62 +7547,229 @@
}
}
},
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.1.0-rc-fb9a90fa48-20240614",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz",
- "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==",
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
}
},
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.13",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz",
- "integrity": "sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ==",
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "eslint": ">=7"
+ "dependencies": {
+ "ms": "^2.1.1"
}
},
- "node_modules/eslint-scope": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
- "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==",
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
+ "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
"dev": true,
- "license": "BSD-2-Clause",
+ "license": "MIT",
"dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
+ "debug": "^3.2.7"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">=4"
},
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
}
},
- "node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
}
},
- "node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "node_modules/eslint-plugin-import": {
+ "version": "2.31.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
+ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.8",
+ "array.prototype.findlastindex": "^1.2.5",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.0",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.15.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.0",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.8",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.1.0-rc-fb9a90fa48-20240614",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz",
+ "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.13",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz",
+ "integrity": "sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=7"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
+ "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
@@ -7667,6 +8160,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
@@ -7733,6 +8247,24 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -7780,6 +8312,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -7835,6 +8384,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -8167,6 +8732,26 @@
"optional": true,
"peer": true
},
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-bigint": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
@@ -8242,6 +8827,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-date-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
@@ -8296,6 +8899,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -8312,7 +8931,6 @@
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"call-bound": "^1.0.3",
"get-proto": "^1.0.0",
@@ -8490,7 +9108,6 @@
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"which-typed-array": "^1.1.16"
},
@@ -8514,6 +9131,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-weakset": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
@@ -9321,6 +9954,59 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
@@ -9358,6 +10044,24 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -10233,6 +10937,29 @@
"node": ">=8"
}
},
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/refractor": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-4.8.1.tgz",
@@ -10489,6 +11216,43 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
@@ -10563,6 +11327,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
@@ -10898,6 +11677,65 @@
"node": ">=8"
}
},
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -11282,11 +12120,88 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
- "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -11320,6 +12235,25 @@
}
}
},
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -11760,6 +12694,34 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/which-collection": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
diff --git a/package.json b/package.json
index cb800c9a..f5b929e8 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,10 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.6",
"@radix-ui/react-visually-hidden": "^1.1.0",
+ "@react-oauth/google": "^0.12.1",
+ "@stepperize/react": "^5.1.5",
"@tanstack/react-query": "^5.59.15",
+ "@tanstack/react-query-devtools": "^5.74.6",
"@tanstack/react-table": "^8.20.5",
"@uiw/react-textarea-code-editor": "^3.1.0",
"@xyflow/react": "^12.3.6",
@@ -78,7 +81,6 @@
"@storybook/react-vite": "^8.6.9",
"@storybook/testing-library": "^0.2.1",
"@storybook/types": "^8.6.9",
- "@tanstack/react-query-devtools": "^5.74.4",
"@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
@@ -94,6 +96,7 @@
"autoprefixer": "^10.4.20",
"chromatic": "^11.27.0",
"eslint": "^9.9.0",
+ "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
diff --git a/src/components/Auth/Auth.tsx b/src/components/Auth/AuthLayout.tsx
similarity index 53%
rename from src/components/Auth/Auth.tsx
rename to src/components/Auth/AuthLayout.tsx
index aaa9d4da..6eea89f0 100644
--- a/src/components/Auth/Auth.tsx
+++ b/src/components/Auth/AuthLayout.tsx
@@ -1,40 +1,20 @@
-import { useEffect, useState } from 'react';
import BGAuthHero from "@/assets/bg-auth-hero.jpg";
import BGNoise from "@/assets/bg-noise.png";
import logoESIFullWhite from "@/assets/logo-esi-full-white.svg";
-import { Button } from "@/components/ui/button";
-import { LucideArrowLeft } from "lucide-react";
-import { WEBSITE_DOMAIN } from '@/constants';
-import AuthSignupSection from '@/components/Auth/AuthSignupSection';
-import AuthLoginSection from '@/components/Auth/AuthLoginSection';
-
-interface AuthProps {
- variant: 'login' | 'signup';
-}
-
-function Auth({ variant }: AuthProps) {
- const [tenantId, setTenantId] = useState('');
- const [currentStep, setCurrentStep] = useState<"organizationURL" | "credentials">("organizationURL");
-
- useEffect(() => {
- setCurrentStep("organizationURL");
- setTenantId('');
- }, [variant]);
-
- const handleStepChange = (step: "organizationURL" | "credentials") => {
- setCurrentStep(step);
- };
-
- const handleOrganizationURLChange = (tenantId: string) => {
- setTenantId(tenantId);
- };
-
+import { WEBSITE_DOMAIN } from "@/constants";
+import { Link, Outlet } from "react-router-dom";
+
+/**
+ * Provides the common layout structure for authentication pages (Login, Signup, etc.).
+ * Includes background elements, branding, and a placeholder for page-specific content.
+ */
+export function AuthLayout() {
return (
-
-
-
- {variant === 'login' ?
-
- :
-
- }
+
-
-
+
+
+
+
-
+
+
@@ -91,5 +53,3 @@ function Auth({ variant }: AuthProps) {
);
}
-
-export default Auth;
\ No newline at end of file
diff --git a/src/components/Auth/AuthLoginSection.tsx b/src/components/Auth/AuthLoginSection.tsx
deleted file mode 100644
index d88f732c..00000000
--- a/src/components/Auth/AuthLoginSection.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { APP_DOMAIN_STRIPPED } from '@/constants';
-import LoginForm from './LoginForm';
-
-interface AuthLoginSectionProps {
- currentStep: "organizationURL" | "credentials";
- organizationUrl: string;
- handleStepChange: (step: "organizationURL" | "credentials") => void;
- handleOrganizationURLChange: (organizationUrl: string) => void;
-}
-
-const AuthLoginSection: React.FC
= ({ currentStep, organizationUrl, handleStepChange, handleOrganizationURLChange }) => (
-
-
-
- {currentStep === "credentials" && organizationUrl ? (
- <>You're logging in on>
- ) : (
- <>Log in to an Organization>
- )}
-
- {currentStep === "credentials" && organizationUrl ? (
-
- {APP_DOMAIN_STRIPPED}/{organizationUrl}
-
- ) : (
-
- Welcome back!
-
- )}
-
-
-
-);
-
-export default AuthLoginSection;
\ No newline at end of file
diff --git a/src/components/Auth/AuthRedirectGuard.tsx b/src/components/Auth/AuthRedirectGuard.tsx
new file mode 100644
index 00000000..b1ad97b4
--- /dev/null
+++ b/src/components/Auth/AuthRedirectGuard.tsx
@@ -0,0 +1,46 @@
+import { IUserData } from "@/types";
+import React, { useEffect, useState } from "react";
+import useAuthUser from "react-auth-kit/hooks/useAuthUser";
+import useIsAuthenticated from "react-auth-kit/hooks/useIsAuthenticated";
+import { Navigate, Outlet } from "react-router-dom";
+
+/**
+ * A layout guard component specifically for auth routes (/login, /signup).
+ * If the user is authenticated (and has an org), it redirects them to the main app (/org/agents).
+ * If the user is not authenticated, it renders an to display the nested route (AuthLayout).
+ * Returns null during the loading state to prevent content flash.
+ */
+export const AuthRedirectGuard: React.FC = () => {
+ const isAuthenticated = useIsAuthenticated();
+ const authUser = useAuthUser();
+ const [redirectTo, setRedirectTo] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let targetPath: string | null = null;
+ if (isAuthenticated) {
+ const user = authUser;
+ const organizationURL = user?.tenant;
+ if (organizationURL) {
+ targetPath = `/${organizationURL}/agents`;
+ } else {
+ console.error(
+ "AuthRedirectGuard: User is authenticated but tenant information is missing."
+ );
+ }
+ }
+
+ setRedirectTo(targetPath);
+ setLoading(false);
+ }, [isAuthenticated, authUser]);
+
+ if (loading) {
+ return null;
+ }
+
+ if (redirectTo) {
+ return ;
+ }
+
+ return ;
+};
diff --git a/src/components/Auth/AuthSignupSection.tsx b/src/components/Auth/AuthSignupSection.tsx
deleted file mode 100644
index 8da5fb96..00000000
--- a/src/components/Auth/AuthSignupSection.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import SignupForm from './SignupForm';
-
-const AuthSignupSection: React.FC = () => (
-
-
- Create an Organization
-
- Unlock the full potential of External Secrets in your Kubernetes cluster
-
-
-
-
-
-);
-
-export default AuthSignupSection;
\ No newline at end of file
diff --git a/src/components/Auth/ForgotPassword.tsx b/src/components/Auth/ForgotPassword.tsx
deleted file mode 100644
index e2c6e3b9..00000000
--- a/src/components/Auth/ForgotPassword.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { Button } from "@/components/ui/button";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { LucideLoader } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import { useForm } from "react-hook-form";
-import { Link, useLocation } from "react-router-dom";
-import { toast } from "sonner";
-import { z } from "zod";
-import zValidations from "./fields/zValidations";
-import AppLogo from "@/components/AppLogo";
-import Cookies from 'js-cookie';
-import { APP_DOMAIN_STRIPPED, ONE_MINUTE_IN_SECONDS, ONE_SECOND_IN_MILLISECONDS } from "@/constants";
-import useForgotPassword from "@/services/forgotPassword/mutations/useForgotPassword";
-
-const ForgotPasswordSchema = z.object({
- tenant: zValidations.organizationURL,
- email: zValidations.email,
-});
-
-type ForgotPasswordData = z.infer;
-
-function ForgotPassword() {
- const [formError, setFormError] = useState("")
- const location = useLocation()
- const toastIdRef = useRef(null);
-
- const state = location.state as { organizationURL?: string; email?: string };
- const defaultTenant = state?.organizationURL || "";
- const defaultEmail = state?.email || "";
-
- const { mutate: forgotPassword, isPending: loading } = useForgotPassword({
- onSuccess: (_, variables: ForgotPasswordData) => {
- // TODO: Remove these cookies when we are sending the necessary data from the token within the reset password email link
- const tenMinutesFromNow = new Date(new Date().getTime() + 10 * ONE_MINUTE_IN_SECONDS * ONE_SECOND_IN_MILLISECONDS);
- Cookies.set("forgotPasswordHelperOrganizationURL", variables.tenant, { expires: tenMinutesFromNow });
- Cookies.set("forgotPasswordHelperEmail", variables.email, { expires: tenMinutesFromNow });
-
- toastIdRef.current = toast.success('Check your email', {
- description: 'We sent instructions to reset your password',
- duration: Infinity,
- cancel: {
- label: 'Dismiss',
- onClick: () => {},
- },
- });
- },
- onError: () => {
- setFormError("Failed to start the reset password flow");
- }
- });
-
- const form = useForm({
- resolver: zodResolver(ForgotPasswordSchema),
- defaultValues: {
- email: defaultEmail,
- tenant: defaultTenant,
- },
- });
-
- const emailInputRef = useRef(null);
- const tenantInputRef = useRef(null);
- const submitButtonRef = useRef(null);
-
- useEffect(() => {
- if (toastIdRef.current) {
- toast.dismiss(toastIdRef.current);
- }
-
- if (defaultTenant) {
- if (!defaultEmail || !zValidations.email.safeParse(defaultEmail).success) {
- emailInputRef.current?.focus();
- } else {
- submitButtonRef.current?.focus();
- }
- } else {
- tenantInputRef.current?.focus();
- }
- }, [location, defaultTenant, defaultEmail])
-
- async function onSubmit(values: ForgotPasswordData) {
- forgotPassword(values);
- }
-
- return (
-
-
-
-
-
-
- {formError &&
{formError}
}
-
-
- );
-}
-
-export default ForgotPassword;
diff --git a/src/components/Auth/LoginCredentialsStep.tsx b/src/components/Auth/LoginCredentialsStep.tsx
deleted file mode 100644
index ee5902c3..00000000
--- a/src/components/Auth/LoginCredentialsStep.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { useFormContext } from "react-hook-form";
-import { Link } from "react-router-dom";
-import { LucideEye, LucideEyeOff, LucideLoader } from "lucide-react";
-
-interface LoginCredentialsStepProps {
- onSubmit: () => void;
- onBack: () => void;
- loading: boolean;
-}
-
-export function LoginCredentialsStep({ onSubmit, onBack, loading }: LoginCredentialsStepProps) {
- const { control, handleSubmit, getValues } = useFormContext();
- const [passwordVisible, setPasswordVisible] = useState(false);
-
- const togglePasswordVisibility = () => {
- setPasswordVisible(!passwordVisible);
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/src/components/Auth/LoginForm.tsx b/src/components/Auth/LoginForm.tsx
deleted file mode 100644
index 14e1f068..00000000
--- a/src/components/Auth/LoginForm.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { trackLoginStepCompleted, trackLoginStepMovedBack, trackSignedIn } from "@/analytics";
-import { FormProvider, useForm } from "react-hook-form";
-import { useState } from "react";
-import useSignIn from "react-auth-kit/hooks/useSignIn";
-import { Link, useNavigate } from "react-router-dom";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { AxiosError, isAxiosError } from "axios";
-import { LoginOrganizationURLStep } from "./LoginOrganizationURLStep";
-import { LoginCredentialsStep } from "./LoginCredentialsStep";
-import zValidations from "./fields/zValidations";
-import { ApiHttpError } from "@/types";
-import { LoginAndIdentifyParams } from "@/services/auth/Auth.interfaces";
-import useLoginAndIdentifyUser from "@/services/auth/mutations/useLoginAndIdentifyUser";
-
-const LoginOrganizationURLSchema = z.object({
- organizationURL: zValidations.organizationURL
-});
-
-const LoginCredentialsSchema = z.object({
- email: zValidations.email,
- password: zValidations.existingPassword,
-});
-
-type LoginOrganizationURLData = z.infer;
-type LoginCredentialsData = z.infer;
-type LoginData = LoginOrganizationURLData & LoginCredentialsData;
-type Step = "organizationURL" | "credentials";
-
-interface LoginFormProps {
- onStepChange: (step: Step) => void;
- onOrganizationURLChange: (tenantId: string) => void;
-}
-
-function LoginForm({ onStepChange, onOrganizationURLChange }: LoginFormProps) {
- const [step, setStep] = useState("organizationURL");
- const [formError, setFormError] = useState(null);
- const authKitSignIn = useSignIn();
- const navigate = useNavigate();
-
- const { mutate: loginAndIdentifyUser, isPending: isLoginPending } = useLoginAndIdentifyUser({
- onError: (error: AxiosError) => {
- const stockError = "Login failed. Please try again.";
- if (isAxiosError(error)) {
- const responseError = error.response?.data?.errors?.error;
- if (responseError?.includes("invalid username/password")) {
- return setFormError("Invalid login credentials");
- }
-
- if (responseError?.includes("invalid tenant")) {
- setStep("organizationURL");
- onStepChange("organizationURL");
- return formMethods.setError("organizationURL", { type: "manual", message: "Invalid Organization URL" });
- }
- }
-
- console.error("Non-Axios error:", error);
- setFormError(stockError);
- },
- onSuccess: (_, variables: LoginAndIdentifyParams) => {
- trackSignedIn(variables.tenantSlug);
- return navigate(`/${variables.tenantSlug}/agents`);
- },
- })
-
- const formMethods = useForm({
- resolver: zodResolver(step === "organizationURL" ? LoginOrganizationURLSchema : LoginCredentialsSchema),
- defaultValues: {
- organizationURL: "",
- email: "",
- password: "",
- },
- });
-
- const handleOrganizationURLStepSubmit = () => {
- const formData = formMethods.getValues();
- onOrganizationURLChange(formData.organizationURL);
- trackLoginStepCompleted(1);
- setStep("credentials");
- onStepChange("credentials");
- };
-
- const handleCredentialsStepSubmit = async (data: LoginCredentialsData) => {
- formMethods.clearErrors();
- setFormError(null);
-
- const formData = formMethods.getValues();
- loginAndIdentifyUser({
- email: data.email!,
- password: data.password!,
- tenantSlug: formData.organizationURL!,
- authKitSignIn,
- });
- };
-
- const handleBack = () => {
- setStep("organizationURL");
- onStepChange("organizationURL");
- trackLoginStepMovedBack();
- };
-
- return (
- <>
-
- {step === "organizationURL" ? (
-
- ) : (
-
- )}
- {formError ? {formError}
: null}
-
-
-
- Don't have an Organization yet?{" "}
-
- Sign up for one
-
-
- >
- );
-}
-
-export default LoginForm;
diff --git a/src/components/Auth/LoginOrganizationURLStep.tsx b/src/components/Auth/LoginOrganizationURLStep.tsx
deleted file mode 100644
index bd448d1f..00000000
--- a/src/components/Auth/LoginOrganizationURLStep.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Button } from "@/components/ui/button";
-import {
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { useFormContext } from "react-hook-form";
-import { useRef } from "react";
-import { APP_DOMAIN_STRIPPED } from "@/constants";
-
-interface LoginOrganizationURLStepProps {
- onSubmit: () => void;
-}
-
-export function LoginOrganizationURLStep({ onSubmit }: LoginOrganizationURLStepProps) {
- const { control, handleSubmit } = useFormContext();
- const orgURLref = useRef(null);
-
- return (
-
- );
-}
diff --git a/src/components/Auth/ResetPassword.tsx b/src/components/Auth/ResetPassword.tsx
deleted file mode 100644
index 68e8ea63..00000000
--- a/src/components/Auth/ResetPassword.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { Button } from "@/components/ui/button";
-import {
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { LucideLoader } from "lucide-react";
-import { useRef, useState, useEffect } from "react";
-import { useForm, FormProvider } from "react-hook-form";
-import { useNavigate, useSearchParams } from "react-router-dom";
-import { toast } from "sonner";
-import { z } from "zod";
-import NewPasswordField from "./fields/NewPasswordField";
-import zValidations from "./fields/zValidations";
-import AppLogo from "@/components/AppLogo";
-import Cookies from 'js-cookie';
-import { APP_DOMAIN_STRIPPED } from "@/constants";
-import useResetPassword from "@/services/forgotPassword/mutations/useResetPassword";
-
-const ResetPasswordSchema = z.object({
- tenant: zValidations.organizationURL,
- email: zValidations.email,
- token: z.string(),
- password: zValidations.newPassword,
-});
-
-type ResetPasswordData = z.infer;
-
-function ResetPasswordForm() {
- // eslint-disable-next-line prefer-const, @typescript-eslint/no-unused-vars
- let [searchParams, _] = useSearchParams();
-
- const navigate = useNavigate();
- const [formError, setFormError] = useState("");
- const [submittedWithErrors, setSubmittedWithErrors] = useState(false);
-
- const { mutate: resetPassword, isPending: loading} = useResetPassword({
- onSuccess: () => {
- toast.success('Password updated successfully', { description: 'Please log in' });
- Cookies.remove("forgotPasswordHelperOrganizationURL");
- Cookies.remove("forgotPasswordHelperEmail");
- navigate('/login')
- }
- });
-
- const newPasswordRef = useRef(null);
-
- const formMethods = useForm({
- resolver: zodResolver(ResetPasswordSchema),
- defaultValues: {
- // TODO: Remove these cookies when we are sending the necessary data from the token within the reset password email link
- tenant: Cookies.get("forgotPasswordHelperOrganizationURL") || "",
- email: Cookies.get("forgotPasswordHelperEmail") || "",
- token: searchParams.get("token") || "",
- password: "",
- },
- });
-
- const { setFocus } = formMethods;
-
- useEffect(() => {
- // TODO: Remove these cookies when we are sending the necessary data from the token within the reset password email link
- const tenant = Cookies.get("forgotPasswordHelperOrganizationURL");
- const email = Cookies.get("forgotPasswordHelperEmail");
-
- if (tenant && email) {
- newPasswordRef.current?.focus();
- } else {
- setFocus("tenant");
- }
- }, [setFocus]);
-
- const handleSubmit = (data: ResetPasswordData) => {
- setSubmittedWithErrors(false);
- onSubmit(data);
- };
-
- const handleError = () => {
- setSubmittedWithErrors(true);
- setFormError("Failed to start the reset password flow")
- };
-
- async function onSubmit(values: ResetPasswordData) {
- resetPassword(values)
- }
-
- return (
-
-
-
-
-
-
-
- {formError &&
{formError}
}
-
-
- );
-}
-
-export default ResetPasswordForm;
diff --git a/src/components/Auth/SignupCredentialsStep.tsx b/src/components/Auth/SignupCredentialsStep.tsx
deleted file mode 100644
index 46cc889f..00000000
--- a/src/components/Auth/SignupCredentialsStep.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useState } from "react";
-import { useFormContext } from "react-hook-form";
-import {
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { LucideLoader } from "lucide-react";
-import NewPasswordField from "./fields/NewPasswordField";
-
-const SignupCredentialsStep = ({ onSubmit, onBack, loading }: { onSubmit: (data: any) => void, onBack: () => void, loading: boolean }) => { // eslint-disable-line @typescript-eslint/no-explicit-any
- const { handleSubmit, control } = useFormContext();
- const [submittedWithErrors, setSubmittedWithErrors] = useState(false);
-
- const handleFormSubmit = (data: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
- setSubmittedWithErrors(false);
- onSubmit(data);
- };
-
- const handleError = () => {
- setSubmittedWithErrors(true);
- };
-
- return (
-
- );
-};
-
-export default SignupCredentialsStep;
diff --git a/src/components/Auth/SignupForm.tsx b/src/components/Auth/SignupForm.tsx
deleted file mode 100644
index 5a359a79..00000000
--- a/src/components/Auth/SignupForm.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { trackSignedIn, trackSignupStepCompleted, trackSignupStepMovedBack } from "@/analytics";
-import { useState } from "react";
-import useSignIn from "react-auth-kit/hooks/useSignIn";
-import { useForm, FormProvider } from "react-hook-form";
-import { Link, useNavigate } from "react-router-dom";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import SignupOrganizationInfoStep from "./SignupOrganizationInfoStep";
-import SignupCredentialsStep from "./SignupCredentialsStep";
-import zValidations from "./fields/zValidations";
-import { AxiosError, isAxiosError } from "axios";
-import { toast } from "sonner";
-import useSignup from "@/services/auth/mutations/useSignup";
-import { ApiHttpError } from "@/types";
-import useLoginAndIdentifyUser from "@/services/auth/mutations/useLoginAndIdentifyUser";
-import { LoginAndIdentifyParams, SignupPayload } from "@/services/auth/Auth.interfaces";
-
-const MAX_LOGIN_RETRIES = 4;
-
-const OrganizationInfoSchema = z.object({
- organizationName: zValidations.organizationName,
- name: zValidations.name,
- organizationURL: zValidations.organizationURL,
-});
-
-const CredentialsSchema = z.object({
- email: zValidations.email,
- password: zValidations.newPassword,
-});
-
-type SignupData = z.infer & z.infer;
-type Step = "organizationInfo" | "credentials";
-
-function SignupForm() {
- const [step, setStep] = useState("organizationInfo");
- const [formError, setFormError] = useState(null);
- const authKitSignIn = useSignIn();
- const navigate = useNavigate();
-
- const { mutate: loginAndIdentifyUser, isPending: isLoginPending } = useLoginAndIdentifyUser({
- onSuccess: (_, variables: LoginAndIdentifyParams) => {
- trackSignedIn(variables.tenantSlug);
- return navigate(`/${variables.tenantSlug}/agents`);
- },
- retry: (failureCount) => {
- if (failureCount < MAX_LOGIN_RETRIES) return true
-
- // If the user has reached the maximum login retries, redirect to login screen
- toast.success('Organization created successfully', {
- description: 'You can now log in with your credentials',
- });
- navigate('/login');
- return false
- }
- })
-
- const { mutate: signup, isPending: isSignupPending } = useSignup({
- onError: (error: AxiosError) => {
- const stockError = "Signup failed. Please try again.";
- if (isAxiosError(error)) {
- const responseError = error.response?.data?.errors?.error;
-
- if (responseError?.includes("tenant name already exists")) {
- setStep("organizationInfo");
- formMethods.setError("organizationURL", { type: "manual", message: "This Organization URL is taken. Create a unique one or log in." });
- // setTimeout is used to ensure the focus is set after the step set is rendered. Not sure what is the Reacty way to do this.
- return setTimeout(() => {
- formMethods.setFocus("organizationURL");
- }, 0);
-
- }
-
- if (responseError?.includes("Field validation for 'Password' failed on the 'password_regex' tag")) {
- formMethods.setError("password", { type: "manual", message: "Invalid special character. Use only: _ ! @ # $ % ^ & * ( ) -" });
- return formMethods.setFocus("password"); // TODO: This is not working, need to investigate. Maybe because of being inside it's own component?
- }
- }
-
- console.error("Non-Axios error:", error);
- setFormError(stockError);
- },
- onSuccess: (_, variables: SignupPayload) => {
- trackSignupStepCompleted(2, variables.tenant);
- loginAndIdentifyUser({
- email: variables.email,
- password: variables.password,
- tenantSlug: variables.tenant,
- name: variables.name,
- authKitSignIn,
- })
- },
- })
-
-
- const formMethods = useForm({
- resolver: zodResolver(step === "organizationInfo" ? OrganizationInfoSchema : CredentialsSchema),
- mode: "onSubmit",
- defaultValues: {
- organizationName: "",
- name: "",
- organizationURL: "",
- email: "",
- password: "",
- },
- });
-
- const handleOrganizationInfoSubmit = () => {
- trackSignupStepCompleted(1, formMethods.getValues("organizationName"));
- setStep("credentials");
- };
-
- const handleCredentialsSubmit = async () => {
- formMethods.clearErrors();
- setFormError(null);
- const formData = formMethods.getValues();
-
- signup({
- email: formData.email,
- name: formData.name,
- password: formData.password,
- tenant: formData.organizationURL
- })
- };
-
- const handleBack = () => {
- setStep("organizationInfo");
- trackSignupStepMovedBack();
- };
-
- return (
- <>
-
- {step === "organizationInfo" ? (
-
- ) : (
-
- )}
- {formError && {formError}
}
-
-
-
- Already a member of an Organization?{" "}
-
- Log in
-
-
- >
- );
-}
-
-export default SignupForm;
diff --git a/src/components/Auth/SignupOrganizationInfoStep.tsx b/src/components/Auth/SignupOrganizationInfoStep.tsx
deleted file mode 100644
index 076fcb34..00000000
--- a/src/components/Auth/SignupOrganizationInfoStep.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { useRef, useState } from "react";
-import { useFormContext } from "react-hook-form";
-import {
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { APP_DOMAIN_STRIPPED } from "@/constants";
-import { createSlug } from "@/utils/slugify";
-
-const SignupOrganizationInfoStep = ({ onSubmit }: { onSubmit: () => void }) => {
- const { handleSubmit, setValue, control } = useFormContext();
- const orgURLRef = useRef(null);
- const [isURLManuallyEdited, setIsURLManuallyEdited] = useState(false);
-
- const handleOrganizationNameChange = (
- e: React.ChangeEvent
- ) => {
- const value = e.target.value;
- setValue("organizationName", value);
- if (!isURLManuallyEdited) {
- setValue("organizationURL", createSlug(value));
- }
- };
-
- const handleOrganizationURLChange = (
- e: React.ChangeEvent
- ) => {
- setIsURLManuallyEdited(true);
- setValue("organizationURL", e.target.value);
- };
-
- return (
-
- );
-};
-
-export default SignupOrganizationInfoStep;
\ No newline at end of file
diff --git a/src/components/Auth/common/AuthCommonFieldEmail.tsx b/src/components/Auth/common/AuthCommonFieldEmail.tsx
new file mode 100644
index 00000000..7b64a573
--- /dev/null
+++ b/src/components/Auth/common/AuthCommonFieldEmail.tsx
@@ -0,0 +1,46 @@
+import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { InputHTMLAttributes, RefObject } from "react";
+import { Control, FieldValues, Path } from "react-hook-form";
+
+interface AuthCommonFieldEmailProps extends Omit, 'name'> {
+ control: Control;
+ name: Path;
+ label?: string;
+ inputRef?: RefObject;
+}
+
+export function AuthCommonFieldEmail({
+ control,
+ name,
+ label = "Email",
+ inputRef,
+ ...props
+}: AuthCommonFieldEmailProps) {
+ return (
+ (
+
+ {label}
+
+
+
+
+
+ )}
+ />
+ );
+}
diff --git a/src/components/Auth/common/AuthCommonFieldNewPassword.tsx b/src/components/Auth/common/AuthCommonFieldNewPassword.tsx
new file mode 100644
index 00000000..f8626fb2
--- /dev/null
+++ b/src/components/Auth/common/AuthCommonFieldNewPassword.tsx
@@ -0,0 +1,54 @@
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import InputPassword from "@/components/ui/InputPassword";
+import { InputHTMLAttributes, RefObject } from "react";
+import { Control, FieldValues, Path } from "react-hook-form";
+
+interface AuthCommonFieldNewPasswordProps
+ extends Omit, "name" | "type"> {
+ control: Control;
+ name: Path;
+ label?: string;
+ inputRef?: RefObject;
+ submittedWithErrors?: boolean;
+}
+
+export function AuthCommonFieldNewPassword({
+ control,
+ name,
+ label = "Password",
+ inputRef,
+ submittedWithErrors = false,
+ ...props
+}: AuthCommonFieldNewPasswordProps) {
+ return (
+ (
+
+ {label}
+
+
+
+
+
+ )}
+ />
+ );
+}
diff --git a/src/components/Auth/common/AuthCommonFieldOrganizationURL.tsx b/src/components/Auth/common/AuthCommonFieldOrganizationURL.tsx
new file mode 100644
index 00000000..1f482e67
--- /dev/null
+++ b/src/components/Auth/common/AuthCommonFieldOrganizationURL.tsx
@@ -0,0 +1,84 @@
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { APP_DOMAIN_STRIPPED } from "@/constants";
+import { cn } from "@/lib/utils";
+import mergeRefs from "merge-refs";
+import { InputHTMLAttributes, RefObject, useRef } from "react";
+import { Control, FieldValues, Path } from "react-hook-form";
+
+interface AuthCommonFieldOrganizationURLProps
+ extends Omit, "name" | "type"> {
+ control: Control;
+ name: Path;
+ label?: string;
+ inputRef?: RefObject;
+ placeholder?: string;
+}
+
+export function AuthCommonFieldOrganizationURL({
+ control,
+ name,
+ label = "Enter your Organization URL",
+ inputRef,
+ placeholder = "acme-inc",
+ className = "",
+ ...props
+}: AuthCommonFieldOrganizationURLProps) {
+ const internalRef = useRef(null);
+ const prefix = APP_DOMAIN_STRIPPED;
+
+ return (
+ (
+
+ {label}
+
+ internalRef.current?.focus()}
+ role="presentation"
+ >
+ {prefix && (
+
+ {prefix}/
+
+ )}
+
+
+
+
+
+ )}
+ />
+ );
+}
diff --git a/src/components/Auth/common/AuthCommonFieldPassword.tsx b/src/components/Auth/common/AuthCommonFieldPassword.tsx
new file mode 100644
index 00000000..836c8a6e
--- /dev/null
+++ b/src/components/Auth/common/AuthCommonFieldPassword.tsx
@@ -0,0 +1,76 @@
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import InputPassword from "@/components/ui/InputPassword";
+import { InputHTMLAttributes, RefObject } from "react";
+import { Control, FieldValues, Path, useFormContext } from "react-hook-form";
+import { Link } from "react-router-dom";
+
+interface AuthCommonFieldPasswordProps
+ extends Omit, "name" | "type"> {
+ control: Control;
+ name: Path;
+ label?: string;
+ inputRef?: RefObject;
+ newPasswordChecks?: boolean;
+ showValidationErrors?: boolean;
+ withForgotPassword?: boolean;
+}
+
+export function AuthCommonFieldPassword({
+ control,
+ name,
+ label = "Password",
+ inputRef,
+ newPasswordChecks,
+ showValidationErrors,
+ withForgotPassword = false,
+ ...props
+}: AuthCommonFieldPasswordProps) {
+ const { getValues } = useFormContext();
+
+ return (
+ (
+
+
+ {label}
+ {withForgotPassword && (
+
+ Forgot your password?
+
+ )}
+
+
+
+
+
+
+ )}
+ />
+ );
+}
diff --git a/src/components/Auth/common/AuthCommonSection.tsx b/src/components/Auth/common/AuthCommonSection.tsx
new file mode 100644
index 00000000..e21d6232
--- /dev/null
+++ b/src/components/Auth/common/AuthCommonSection.tsx
@@ -0,0 +1,27 @@
+import { ReactNode } from "react";
+
+interface AuthCommonSectionProps {
+ title: ReactNode;
+ description: ReactNode;
+ children: ReactNode;
+ ariaLabel?: string;
+}
+
+export function AuthCommonSection({
+ title,
+ description,
+ children,
+ ariaLabel,
+}: AuthCommonSectionProps) {
+ return (
+
+
+ {title}
+
+ {description}
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/Auth/common/AuthCommonSubmitButton.tsx b/src/components/Auth/common/AuthCommonSubmitButton.tsx
new file mode 100644
index 00000000..bd72d0ed
--- /dev/null
+++ b/src/components/Auth/common/AuthCommonSubmitButton.tsx
@@ -0,0 +1,42 @@
+import { Button } from "@/components/ui/button";
+import { LucideLoader } from "lucide-react";
+import { ReactNode, RefObject } from "react";
+
+interface AuthCommonSubmitButtonProps {
+ isLoading: boolean;
+ text: ReactNode;
+ className?: string;
+ buttonRef?: RefObject;
+ disabled?: boolean;
+ onClick?: () => void;
+ tabIndex?: number;
+}
+
+export function AuthCommonSubmitButton({
+ isLoading,
+ text,
+ className = "",
+ buttonRef,
+ disabled = false,
+ onClick,
+ tabIndex,
+}: AuthCommonSubmitButtonProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Auth/common/authCommonErrorHandlers.ts b/src/components/Auth/common/authCommonErrorHandlers.ts
new file mode 100644
index 00000000..f7ecc5d6
--- /dev/null
+++ b/src/components/Auth/common/authCommonErrorHandlers.ts
@@ -0,0 +1,112 @@
+import { AxiosError } from "axios";
+import { ApiHttpError } from "@/types";
+
+/**
+ * Common error types that can occur during authentication flows
+ */
+type AuthErrorType =
+ 'validation' | // Field-specific validation failures
+ 'authentication' | // Auth-specific failures like invalid credentials
+ 'api' | // Other API-returned errors
+ 'network' | // Connection issues
+ 'unknown'; // Unrecognized errors
+
+/**
+ * Standardized error structure for auth flows
+ */
+interface AuthError {
+ type: AuthErrorType;
+ message: string;
+ field?: string;
+ originalError?: AxiosError;
+}
+
+/**
+ * Common error messages used across auth flows
+ */
+export const AUTH_ERROR_MESSAGES = {
+ INVALID_CREDENTIALS: "Invalid email or password",
+ PASSWORD_RESET_REQUIRED: "You need to reset your password to continue",
+ NETWORK_ERROR: "Network error. Please check your connection",
+ UNEXPECTED_ERROR: "An unexpected error occurred",
+ TENANT_TAKEN: "Organization URL already taken. Choose another one.",
+ TENANT_NOT_FOUND: "Organization not found",
+ MISSING_REQUIRED_FIELDS: "Please fill in all required fields",
+ INVALID_PASSWORD_FORMAT: "Invalid special character. Use only: _!@#$%^&*()-.",
+} as const;
+
+/**
+ * Detects specific known error patterns from API responses
+ */
+function identifyAuthErrorDetails(error: unknown): {
+ type: AuthErrorType;
+ message: string;
+ field?: string;
+} {
+ if (!(error instanceof AxiosError)) {
+ return {
+ type: 'unknown',
+ message: AUTH_ERROR_MESSAGES.UNEXPECTED_ERROR
+ };
+ }
+
+ if (!error.response) {
+ return {
+ type: 'network',
+ message: AUTH_ERROR_MESSAGES.NETWORK_ERROR
+ };
+ }
+
+ if (error.response.status === 401) {
+ return {
+ type: 'authentication',
+ message: AUTH_ERROR_MESSAGES.INVALID_CREDENTIALS
+ };
+ }
+
+ const apiErrorMessage = error.response?.data?.errors?.error;
+ if (typeof apiErrorMessage !== 'string') {
+ return {
+ type: 'unknown',
+ message: AUTH_ERROR_MESSAGES.UNEXPECTED_ERROR
+ };
+ }
+
+ if (apiErrorMessage.includes('tenant name already exists')) {
+ return {
+ type: 'validation',
+ message: AUTH_ERROR_MESSAGES.TENANT_TAKEN,
+ field: 'organizationURL'
+ };
+ }
+
+ if (apiErrorMessage.includes("Field validation for 'Password' failed on the 'password_regex' tag")) {
+ return {
+ type: 'validation',
+ message: AUTH_ERROR_MESSAGES.INVALID_PASSWORD_FORMAT,
+ field: 'password'
+ };
+ }
+
+ return {
+ type: 'api',
+ message: apiErrorMessage
+ };
+}
+
+/**
+ * Creates a standardized auth error object with intelligent error detection
+ */
+export function createAuthError(
+ error: unknown,
+ field?: string
+): AuthError {
+ const errorDetails = identifyAuthErrorDetails(error);
+
+ return {
+ type: errorDetails.type,
+ message: errorDetails.message,
+ field: field || errorDetails.field,
+ originalError: error instanceof AxiosError ? error : undefined
+ };
+}
\ No newline at end of file
diff --git a/src/components/Auth/common/authCommonZodSchemas.ts b/src/components/Auth/common/authCommonZodSchemas.ts
new file mode 100644
index 00000000..ab8106f5
--- /dev/null
+++ b/src/components/Auth/common/authCommonZodSchemas.ts
@@ -0,0 +1,52 @@
+import { z } from "zod";
+
+// Export each schema individually to avoid temporal dead zone issues
+export const passwordMinLengthValue = 12;
+export const regexIsUppercase = /(?=.*[A-Z])/;
+export const regexIsNumber = /(?=.*[0-9])/;
+export const regexIsSpecialCharacter = /(?=.*[_!@#$%^&*()-])/;
+export const regexAllowedPasswordCharacters = /[a-zA-Z0-9_!@#$%^&*()-]/;
+
+export const regexPasswordPattern = new RegExp(
+ `^${regexIsUppercase.source}${regexIsNumber.source}${regexIsSpecialCharacter.source}${regexAllowedPasswordCharacters.source}{${passwordMinLengthValue},}$`
+);
+
+// Export individual schemas directly to avoid initialization issues
+export const organizationNameSchema = z
+ .string()
+ .min(1, "Cannot be empty");
+
+export const nameSchema = z
+ .string()
+ .min(1, "Cannot be empty");
+
+export const organizationURLSchema = z
+ .string()
+ .min(1, "Cannot be empty.")
+ .regex(/^[a-z0-9-]+$/, "Organization URL may only contain lowercase letters, numbers, and dashes.");
+
+export const emailSchema = z
+ .string()
+ .email("Invalid email address.");
+
+export const newPasswordSchema = z
+ .string()
+ .min(passwordMinLengthValue, `Password must be at least ${passwordMinLengthValue} characters.`)
+ .regex(regexIsUppercase, "Password must contain at least one uppercase letter.")
+ .regex(regexIsNumber, "Password must contain at least one number.")
+ .regex(regexIsSpecialCharacter, "Password must contain at least one special character (_!@#$%^&*()-).");
+
+export const existingPasswordSchema = z
+ .string()
+ .min(1, "Cannot be empty.");
+
+// Export the object containing all schemas for backward compatibility
+// This references the individually exported schemas to avoid duplication
+export const authCommonZodSchemas = {
+ organizationName: organizationNameSchema,
+ name: nameSchema,
+ organizationURL: organizationURLSchema,
+ email: emailSchema,
+ newPassword: newPasswordSchema,
+ existingPassword: existingPasswordSchema,
+};
diff --git a/src/components/Auth/fields/NewPasswordField.tsx b/src/components/Auth/fields/NewPasswordField.tsx
deleted file mode 100644
index e5b32433..00000000
--- a/src/components/Auth/fields/NewPasswordField.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useEffect, useState, forwardRef } from "react";
-import { useFormContext } from "react-hook-form";
-import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { LucideCheckSquare, LucideSquare, LucideEye, LucideEyeOff } from "lucide-react";
-import { regexPasswordPattern, passwordMinLengthValue, regexIsUppercase, regexIsNumber, regexIsSpecialCharacter } from "./zValidations";
-import { Button } from "@/components/ui/button";
-
-interface NewPasswordFieldProps {
- submittedWithErrors: boolean;
-}
-
-const NewPasswordField = forwardRef(
- ({ submittedWithErrors }, ref) => {
- const { getValues, setValue, control } = useFormContext();
- const [password, setPassword] = useState(getValues("password"));
- const [passwordVisible, setPasswordVisible] = useState(false);
- const [passwordValidations, setPasswordValidations] = useState({
- length: password?.length >= passwordMinLengthValue,
- uppercase: regexIsUppercase.test(password),
- number: regexIsNumber.test(password),
- specialChar: regexIsSpecialCharacter.test(password),
- });
-
- useEffect(() => {
- setPasswordValidations({
- length: password?.length >= passwordMinLengthValue,
- uppercase: regexIsUppercase.test(password),
- number: regexIsNumber.test(password),
- specialChar: regexIsSpecialCharacter.test(password),
- });
- }, [password]);
-
- const handlePasswordChange = (e: React.ChangeEvent) => {
- const value = e.target.value;
- setPassword(value);
- setValue("password", value);
- };
-
- const togglePasswordVisibility = () => {
- setPasswordVisible(!passwordVisible);
- };
-
- return (
- (
-
- Password
-
-
-
-
-
-
-
- -
- {passwordValidations.uppercase ? (
-
- ) : (
-
- )}
- At least one uppercase letter
-
- -
- {passwordValidations.number ? (
-
- ) : (
-
- )}
- At least one number
-
- -
- {passwordValidations.specialChar ? (
-
- ) : (
-
- )}
- At least one special character
-
- -
- {passwordValidations.length ? (
-
- ) : (
-
- )}
- At least {passwordMinLengthValue} characters
-
-
-
-
- )}
- />
- );
- }
-);
-
-NewPasswordField.displayName = 'NewPasswordField';
-
-export default NewPasswordField;
diff --git a/src/components/Auth/fields/zValidations.ts b/src/components/Auth/fields/zValidations.ts
deleted file mode 100644
index 478e4e1c..00000000
--- a/src/components/Auth/fields/zValidations.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { z } from "zod";
-
-export const passwordMinLengthValue = 12;
-export const regexIsUppercase = /(?=.*[A-Z])/;
-export const regexIsNumber = /(?=.*[0-9])/;
-export const regexIsSpecialCharacter = /(?=.*[_!@#$%^&*()-])/;
-export const regexAllowedPasswordCharacters = /[a-zA-Z0-9_!@#$%^&*()-]/;
-
-export const regexPasswordPattern = new RegExp(
- `^${regexIsUppercase.source}${regexIsNumber.source}${regexIsSpecialCharacter.source}${regexAllowedPasswordCharacters.source}{${passwordMinLengthValue},}$`
-);
-
-const zValidations = {
- organizationName: z
- .string()
- .min(1, "Cannot be empty"),
- name: z.string().min(1, "Cannot be empty"),
- organizationURL: z
- .string()
- .min(1, "Cannot be empty.")
- .regex(/^[a-z0-9-]+$/, "Organization URL may only contain lowercase letters, numbers, and dashes."),
- email: z.string().email("Invalid email address."),
- newPassword: z
- .string()
- .min(passwordMinLengthValue, `Password must be at least ${passwordMinLengthValue} characters.`)
- .regex(regexIsUppercase, "Password must contain at least one uppercase letter.")
- .regex(regexIsNumber, "Password must contain at least one number.")
- .regex(regexIsSpecialCharacter, "Password must contain at least one special character (_!@#$%^&*()-)."),
- existingPassword: z.string().min(1, "Cannot be empty."),
-}
-
-export default zValidations;
\ No newline at end of file
diff --git a/src/components/Auth/forgotPassword/AuthForgotPassword.tsx b/src/components/Auth/forgotPassword/AuthForgotPassword.tsx
new file mode 100644
index 00000000..892ab00f
--- /dev/null
+++ b/src/components/Auth/forgotPassword/AuthForgotPassword.tsx
@@ -0,0 +1,97 @@
+import {
+ AuthCommonFieldEmail,
+ AuthCommonFieldOrganizationURL,
+ AuthCommonSection,
+ AuthCommonSubmitButton,
+ useAuthForgotPasswordFlow,
+} from "@/components/Auth";
+import { Button } from "@/components/ui/button";
+import { Form } from "@/components/ui/form";
+import { LucideArrowLeft } from "lucide-react";
+import { useEffect } from "react";
+import { Link, useLocation } from "react-router-dom";
+
+export function AuthForgotPassword() {
+ const location = useLocation();
+ const state = location.state as { organizationURL?: string; email?: string };
+ const defaultTenant = state?.organizationURL || "";
+ const defaultEmail = state?.email || "";
+
+ const {
+ form,
+ handleSubmit,
+ formError,
+ isLoading,
+ emailInputRef,
+ tenantInputRef,
+ submitButtonRef,
+ } = useAuthForgotPasswordFlow(defaultTenant, defaultEmail);
+
+ useEffect(
+ function setInitialFormFieldFocus() {
+ if (!defaultTenant) return tenantInputRef.current?.focus();
+ if (!defaultEmail) return emailInputRef.current?.focus();
+ submitButtonRef.current?.focus();
+ },
+ [
+ location,
+ defaultTenant,
+ defaultEmail,
+ emailInputRef,
+ tenantInputRef,
+ submitButtonRef,
+ ]
+ );
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/Auth/forgotPassword/useAuthForgotPasswordFlow.ts b/src/components/Auth/forgotPassword/useAuthForgotPasswordFlow.ts
new file mode 100644
index 00000000..2a581731
--- /dev/null
+++ b/src/components/Auth/forgotPassword/useAuthForgotPasswordFlow.ts
@@ -0,0 +1,98 @@
+import { authCommonZodSchemas, createAuthError } from "@/components/Auth";
+import useForgotPassword from "@/services/forgotPassword/mutations/useForgotPassword";
+import { ApiHttpError } from "@/types";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { AxiosError } from "axios";
+import { useCallback, useRef, useState } from "react";
+import { useForm } from "react-hook-form";
+import { toast } from "sonner";
+import { z } from "zod";
+
+const ForgotPasswordSchema = z.object({
+ tenant: authCommonZodSchemas.organizationURL,
+ email: authCommonZodSchemas.email,
+});
+
+type ForgotPasswordData = z.infer;
+
+export function useAuthForgotPasswordFlow(defaultTenant: string = "", defaultEmail: string = "") {
+ const [formError, setFormError] = useState("");
+ const toastIdRef = useRef(null);
+ const emailInputRef = useRef(null);
+ const tenantInputRef = useRef(null);
+ const submitButtonRef = useRef(null);
+
+ const form = useForm({
+ resolver: zodResolver(ForgotPasswordSchema),
+ defaultValues: {
+ email: defaultEmail,
+ tenant: defaultTenant,
+ },
+ });
+
+ const { mutate: forgotPassword, isPending: isLoading } = useForgotPassword({
+ onSuccess: () => {
+
+ toastIdRef.current = toast.success('Check your email', {
+ description: 'If an account matching your details was found, instructions to reset your password have been sent.',
+ duration: Infinity,
+ cancel: {
+ label: 'Dismiss',
+ onClick: () => { },
+ },
+ });
+ setFormError("");
+ },
+ onError: (error: AxiosError) => {
+ console.error("Forgot password request encountered an error:", error);
+ const authError = createAuthError(error);
+
+ // Determine if this error signifies a "user/org not found" scenario or similar
+ // which should be masked with a generic success message for security reasons.
+ const responseData = error.response?.data as Partial | undefined;
+ const specificApiError = responseData?.errors?.error;
+
+ const shouldMaskError = error instanceof AxiosError &&
+ error.response &&
+ (
+ error.response.status === 401 || // Standard "Unauthorized" often implies not found here
+ error.response.status === 404 || // Standard "Not Found"
+ (error.response.status === 422 && specificApiError === 'invalid username/password') // Current backend specific case
+ );
+
+ if (shouldMaskError) {
+ // For "not found" type errors, show the generic success toast to prevent enumeration
+ toastIdRef.current = toast.success('Check your email', {
+ description: 'If an account matching your details was found, instructions to reset your password have been sent.',
+ duration: Infinity,
+ cancel: { label: 'Dismiss', onClick: () => { } },
+ });
+ setFormError("");
+ } else {
+ setFormError(authError.message);
+ }
+ }
+ });
+
+ const handleSubmit = useCallback((values: ForgotPasswordData) => {
+ setFormError("");
+ forgotPassword(values);
+ }, [forgotPassword]);
+
+ const handleDismissToast = useCallback(() => {
+ if (toastIdRef.current) {
+ toast.dismiss(toastIdRef.current);
+ }
+ }, []);
+
+ return {
+ form,
+ handleSubmit,
+ formError,
+ isLoading,
+ emailInputRef,
+ tenantInputRef,
+ submitButtonRef,
+ handleDismissToast
+ };
+}
\ No newline at end of file
diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts
index 327e1055..8ba2d5df 100644
--- a/src/components/Auth/index.ts
+++ b/src/components/Auth/index.ts
@@ -1,2 +1,56 @@
-export * from './Auth';
-export { default } from './Auth';
\ No newline at end of file
+// Common
+export {
+ emailSchema,
+ existingPasswordSchema,
+ nameSchema,
+ newPasswordSchema,
+ organizationNameSchema,
+ organizationURLSchema,
+ passwordMinLengthValue,
+ regexAllowedPasswordCharacters,
+ regexIsNumber,
+ regexIsSpecialCharacter,
+ regexIsUppercase,
+ regexPasswordPattern,
+ authCommonZodSchemas,
+} from './common/authCommonZodSchemas';
+
+export {
+ AUTH_ERROR_MESSAGES,
+ createAuthError,
+} from './common/authCommonErrorHandlers';
+
+export { AuthCommonFieldEmail } from './common/AuthCommonFieldEmail';
+export { AuthCommonFieldNewPassword } from './common/AuthCommonFieldNewPassword';
+export { AuthCommonFieldOrganizationURL } from './common/AuthCommonFieldOrganizationURL';
+export { AuthCommonFieldPassword } from './common/AuthCommonFieldPassword';
+export { AuthCommonSection } from './common/AuthCommonSection';
+export { AuthCommonSubmitButton } from './common/AuthCommonSubmitButton';
+
+// Root Level
+export { AuthLayout } from './AuthLayout';
+export { AuthRedirectGuard } from './AuthRedirectGuard';
+
+// Login
+export { AuthLogin } from './login/AuthLogin';
+export { AuthLoginForm } from './login/AuthLoginForm';
+export { AuthLoginStepCredentials } from './login/AuthLoginStepCredentials';
+export { AuthLoginStepOrganizationURL } from './login/AuthLoginStepOrganizationURL';
+export { useAuthLoginFlow } from './login/useAuthLoginFlow';
+export { useAuthLoginFormContext } from './login/AuthLoginForm';
+
+// Signup
+export { AuthSignup } from './signup/AuthSignup';
+export { AuthSignupForm } from './signup/AuthSignupForm';
+export { AuthSignupStepCredentials } from './signup/AuthSignupStepCredentials';
+export { AuthSignupStepOrganizationInfo } from './signup/AuthSignupStepOrganizationInfo';
+export { useAuthSignupFlow } from './signup/useAuthSignupFlow';
+export { useAuthSignupFormContext } from './signup/AuthSignupForm';
+
+// Forgot Password
+export { AuthForgotPassword } from './forgotPassword/AuthForgotPassword';
+export { useAuthForgotPasswordFlow } from './forgotPassword/useAuthForgotPasswordFlow';
+
+// Reset Password
+export { AuthResetPassword } from './resetPassword/AuthResetPassword';
+export { useAuthResetPasswordFlow } from './resetPassword/useAuthResetPasswordFlow';
diff --git a/src/components/Auth/login/AuthLogin.tsx b/src/components/Auth/login/AuthLogin.tsx
new file mode 100644
index 00000000..b40e44f8
--- /dev/null
+++ b/src/components/Auth/login/AuthLogin.tsx
@@ -0,0 +1,46 @@
+import { AuthCommonSection, AuthLoginForm } from "@/components/Auth";
+import { APP_DOMAIN_STRIPPED } from "@/constants";
+import { useState } from "react";
+
+export function AuthLogin() {
+ const [stepId, setStepId] = useState("organizationURL");
+ const [orgUrl, setOrgUrl] = useState("");
+
+ const handleStepChange = (newStepId: string) => {
+ setStepId(newStepId);
+ };
+
+ const handleOrganizationURLChange = (newOrgUrl: string) => {
+ setOrgUrl(newOrgUrl);
+ };
+
+ const title =
+ stepId === "credentials" && orgUrl ? (
+ <>You're logging in on>
+ ) : (
+ <>Log in to an Organization>
+ );
+
+ const description =
+ stepId === "credentials" && orgUrl ? (
+
+ {APP_DOMAIN_STRIPPED}/
+ {orgUrl}
+
+ ) : (
+ <>Welcome back!>
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Auth/login/AuthLoginForm.tsx b/src/components/Auth/login/AuthLoginForm.tsx
new file mode 100644
index 00000000..0c7195ab
--- /dev/null
+++ b/src/components/Auth/login/AuthLoginForm.tsx
@@ -0,0 +1,149 @@
+import {
+ authCommonZodSchemas,
+ AuthLoginStepCredentials,
+ AuthLoginStepOrganizationURL,
+ useAuthLoginFlow,
+} from "@/components/Auth";
+import { Form } from "@/components/ui/form";
+import { cn } from "@/lib/utils";
+import { Step, defineStepper } from "@stepperize/react";
+import { createContext, FC, useContext } from "react";
+import { Link } from "react-router-dom";
+import { UseFormReturn } from "react-hook-form";
+import { z } from "zod";
+
+const OrganizationURLSchema = z.object({
+ organizationURL: authCommonZodSchemas.organizationURL,
+});
+const CredentialsSchema = z.object({
+ email: authCommonZodSchemas.email,
+ password: authCommonZodSchemas.existingPassword,
+});
+
+const step1 = {
+ id: "organizationURL",
+ label: "Organization URL",
+ schema: OrganizationURLSchema,
+};
+const step2 = {
+ id: "credentials",
+ label: "Credentials",
+ schema: CredentialsSchema,
+};
+const { Scoped } = defineStepper(step1, step2);
+
+type LoginFormShape = z.infer &
+ z.infer;
+
+interface AuthLoginFormContextType {
+ form: UseFormReturn;
+ currentStep: Step;
+ stepperMethods: {
+ prev: () => void;
+ next: () => void;
+ switch: (cases: Record T>) => T;
+ current: Step;
+ };
+ isProcessing: boolean;
+ handleAttemptSubmit: (data: LoginFormShape) => void;
+ formError: string | null;
+}
+
+const AuthLoginFormContext = createContext<
+ AuthLoginFormContextType | undefined
+>(undefined);
+
+export function useAuthLoginFormContext() {
+ const context = useContext(AuthLoginFormContext);
+ if (!context) {
+ throw new Error(
+ "useAuthLoginFormContext must be used within a AuthLoginForm provider"
+ );
+ }
+ return context;
+}
+
+const AuthLoginFormContent: FC = () => {
+ const { form, stepperMethods, handleAttemptSubmit, formError } =
+ useAuthLoginFormContext();
+
+ return (
+ <>
+
+
+
+
+ Don't have an Organization yet?{" "}
+
+ Sign up for one
+
+
+ >
+ );
+};
+
+interface AuthLoginFormProps {
+ /**
+ * Callback function invoked when the login flow progresses to a new step.
+ * This function is called by `AuthLoginForm` to notify its parent (`AuthLogin`)
+ * about changes to the current step, allowing the parent to update its UI accordingly.
+ * @param step The identifier of the new current step (e.g., "organizationURL", "credentials").
+ */
+ onStepChange: (step: string) => void;
+ /**
+ * Callback function invoked when the organization URL is successfully entered and validated.
+ * This function is called by `AuthLoginForm` to notify its parent (`AuthLogin`)
+ * of the validated organization URL, allowing the parent to update its UI or state.
+ * @param tenantId The validated organization URL (tenant identifier).
+ */
+ onOrganizationURLChange: (tenantId: string) => void;
+}
+
+// 9. Main Exported Component (which now also acts as the Provider)
+export function AuthLoginForm(props: AuthLoginFormProps) {
+ const {
+ form,
+ stepperMethods,
+ handleAttemptSubmit,
+ formError,
+ isLoadingCoreFlow,
+ } = useAuthLoginFlow(props.onStepChange, props.onOrganizationURLChange);
+
+ const currentStep = stepperMethods.current;
+
+ const isProcessing = isLoadingCoreFlow;
+
+ const contextValue = {
+ form,
+ currentStep,
+ stepperMethods,
+ isProcessing,
+ handleAttemptSubmit,
+ formError,
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/Auth/login/AuthLoginStepCredentials.tsx b/src/components/Auth/login/AuthLoginStepCredentials.tsx
new file mode 100644
index 00000000..37ab2c65
--- /dev/null
+++ b/src/components/Auth/login/AuthLoginStepCredentials.tsx
@@ -0,0 +1,58 @@
+import { trackLoginStepMovedBack } from "@/analytics";
+import {
+ AuthCommonFieldEmail,
+ AuthCommonFieldPassword,
+ AuthCommonSubmitButton,
+ useAuthLoginFormContext,
+} from "@/components/Auth";
+import { Button } from "@/components/ui/button";
+import { LucideArrowLeft } from "lucide-react";
+import { useFormContext } from "react-hook-form";
+
+export function AuthLoginStepCredentials() {
+ const { control } = useFormContext();
+ const {
+ isProcessing,
+ stepperMethods,
+ } = useAuthLoginFormContext();
+ const { prev } = stepperMethods;
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/Auth/login/AuthLoginStepOrganizationURL.tsx b/src/components/Auth/login/AuthLoginStepOrganizationURL.tsx
new file mode 100644
index 00000000..a87a66da
--- /dev/null
+++ b/src/components/Auth/login/AuthLoginStepOrganizationURL.tsx
@@ -0,0 +1,29 @@
+import {
+ AuthCommonFieldOrganizationURL,
+ AuthCommonSubmitButton,
+ useAuthLoginFormContext,
+} from "@/components/Auth";
+import { useFormContext } from "react-hook-form";
+
+export function AuthLoginStepOrganizationURL() {
+ const { control } = useFormContext();
+ const { isProcessing } = useAuthLoginFormContext();
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/components/Auth/login/useAuthLoginFlow.ts b/src/components/Auth/login/useAuthLoginFlow.ts
new file mode 100644
index 00000000..87ebc719
--- /dev/null
+++ b/src/components/Auth/login/useAuthLoginFlow.ts
@@ -0,0 +1,208 @@
+import { trackLoginStepCompleted, trackSignedIn, trackLoginStepMovedBack } from "@/analytics";
+import { AUTH_ERROR_MESSAGES, authCommonZodSchemas, createAuthError } from "@/components/Auth";
+import type { LoginResult } from "@/services/auth/mutations/useLoginAndIdentifyUser";
+import useLoginAndIdentifyUser from "@/services/auth/mutations/useLoginAndIdentifyUser";
+import type { LoginAndIdentifyParams } from "@/services/auth/Auth.interfaces";
+import { isTenantRegistered, isTenantAvailable, useGetTenantByName } from "@/services/tenants/queries/useGetTenantByName";
+import { ApiHttpError } from "@/types";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { defineStepper, Step } from "@stepperize/react";
+import { AxiosError } from "axios";
+import { useCallback, useEffect, useState } from "react";
+import useSignIn from "react-auth-kit/hooks/useSignIn";
+import { useForm } from "react-hook-form";
+import { useNavigate } from "react-router-dom";
+import { z } from "zod";
+import { toast } from "sonner";
+
+const OrganizationURLSchema = z.object({
+ organizationURL: authCommonZodSchemas.organizationURL,
+});
+
+const CredentialsSchema = z.object({
+ email: authCommonZodSchemas.email,
+ password: authCommonZodSchemas.existingPassword,
+});
+
+type OrgURLData = z.infer;
+type CredentialsData = z.infer;
+type LoginData = OrgURLData & CredentialsData;
+
+const loginSteps = [
+ { id: "organizationURL", label: "Organization URL", schema: OrganizationURLSchema },
+ { id: "credentials", label: "Credentials", schema: CredentialsSchema }
+];
+
+const { useStepper } = defineStepper(...loginSteps);
+
+export function useAuthLoginFlow(
+ onStepChange: (stepId: string) => void,
+ onOrganizationURLChange: (tenantId: string) => void
+) {
+ const [formError, setFormError] = useState(null);
+ const [shouldCheckTenant, setShouldCheckTenant] = useState(false);
+ const [tenantNameToCheckQuery, setTenantNameToCheckQuery] = useState("");
+
+ const authKitSignIn = useSignIn();
+ const navigate = useNavigate();
+ const stepperMethods = useStepper();
+ const { next, isLast, current: currentStep, all: steps, prev } = stepperMethods;
+
+ const form = useForm({
+ resolver: zodResolver(currentStep.schema),
+ defaultValues: {
+ organizationURL: "",
+ email: "",
+ password: "",
+ },
+ });
+
+ const {
+ data: tenantCheckResult,
+ error: tenantError,
+ status: tenantStatus,
+ isFetching: isCheckingTenant
+ } = useGetTenantByName(
+ tenantNameToCheckQuery,
+ {
+ enabled: shouldCheckTenant && !!tenantNameToCheckQuery,
+ throwOnError: false
+ }
+ );
+
+ const { mutate: loginAndIdentifyUser, isPending: isLoginPending } = useLoginAndIdentifyUser({
+ onError: (error: AxiosError, variables: LoginAndIdentifyParams) => {
+ const authError = createAuthError(error);
+
+ if (error.response?.status === 403 && authError.message?.toLowerCase() === "password reset required") {
+ toast.info(AUTH_ERROR_MESSAGES.PASSWORD_RESET_REQUIRED);
+ navigate('/reset-password', {
+ state: {
+ tenant: variables.tenantSlug,
+ email: variables.email
+ }
+ });
+ return;
+ }
+
+ if (authError.type === 'validation' && authError.field) {
+ if (authError.field === 'email') {
+ form.setError('email', { type: "manual", message: authError.message });
+ setTimeout(() => { form.setFocus('email'); }, 0);
+ return;
+ }
+ if (authError.field === 'password') {
+ form.setError('password', { type: "manual", message: authError.message });
+ setTimeout(() => { form.setFocus('password'); }, 0);
+ return;
+ }
+ }
+
+ setFormError(authError.message);
+ },
+ onSuccess: (data: LoginResult, variables: LoginAndIdentifyParams) => {
+ if (data.isSignedIn) {
+ trackSignedIn(variables.tenantSlug);
+ navigate('/');
+ } else {
+ setFormError("Login failed after identification.");
+ }
+ },
+ });
+
+ const handleStepChange = useCallback(() => {
+ onStepChange(currentStep.id);
+
+ if (currentStep.id === 'credentials') {
+ const formData = form.getValues();
+ if (typeof formData.organizationURL === 'string') {
+ onOrganizationURLChange(formData.organizationURL);
+ }
+ }
+
+ setFormError(null);
+
+ form.reset(form.getValues(), {
+ keepValues: true,
+ keepDirty: true,
+ keepErrors: false,
+ keepTouched: false,
+ keepIsSubmitted: false,
+ keepIsValid: false,
+ keepSubmitCount: false
+ });
+ }, [currentStep.id, onStepChange, onOrganizationURLChange, form]);
+
+ useEffect(() => {
+ handleStepChange();
+ }, [handleStepChange]);
+
+ useEffect(() => {
+ if (!shouldCheckTenant || isCheckingTenant) {
+ return;
+ }
+
+ if (tenantStatus === 'success') {
+ if (isTenantRegistered(tenantCheckResult)) {
+ trackLoginStepCompleted(steps.findIndex((s: Step) => s.id === currentStep.id) + 1);
+ next();
+ } else if (isTenantAvailable(tenantCheckResult)) {
+ form.setError("organizationURL", {
+ type: "manual",
+ message: AUTH_ERROR_MESSAGES.TENANT_NOT_FOUND.replace("Organization", `Organization '${tenantNameToCheckQuery}'`)
+ });
+ setTimeout(() => { form.setFocus("organizationURL"); }, 0);
+ }
+ } else if (tenantStatus === 'error') {
+ const authError = createAuthError(tenantError);
+ setFormError(authError.message);
+ }
+
+ setShouldCheckTenant(false);
+ }, [tenantStatus, tenantCheckResult, tenantError, shouldCheckTenant, steps, currentStep.id, next, form, tenantNameToCheckQuery, isCheckingTenant]);
+
+ const handleAttemptSubmit = useCallback(async (data: Partial) => {
+ setFormError(null);
+
+ if (currentStep.id === 'organizationURL') {
+ const tenantName = data.organizationURL;
+ if (!tenantName) {
+ form.trigger("organizationURL");
+ return;
+ }
+ setTenantNameToCheckQuery(tenantName);
+ setShouldCheckTenant(true);
+ } else if (currentStep.id === 'credentials' && isLast) {
+ const formData = { ...form.getValues(), ...data };
+ if (!formData.email || !formData.password || !formData.organizationURL) {
+ setFormError(AUTH_ERROR_MESSAGES.MISSING_REQUIRED_FIELDS);
+ return;
+ }
+
+ loginAndIdentifyUser({
+ email: formData.email,
+ password: formData.password,
+ tenantSlug: String(formData.organizationURL),
+ authKitSignIn,
+ });
+ } else if (!isLast) {
+ next();
+ }
+ }, [currentStep, isLast, next, form, loginAndIdentifyUser, authKitSignIn, setTenantNameToCheckQuery, setShouldCheckTenant]);
+
+ const handleGoBack = useCallback(() => {
+ prev();
+ trackLoginStepMovedBack();
+ }, [prev]);
+
+ const isLoadingCoreFlow = isLoginPending || isCheckingTenant;
+
+ return {
+ form,
+ stepperMethods,
+ handleAttemptSubmit,
+ handleGoBack,
+ formError,
+ isLoadingCoreFlow,
+ };
+}
\ No newline at end of file
diff --git a/src/components/Auth/resetPassword/AuthResetPassword.tsx b/src/components/Auth/resetPassword/AuthResetPassword.tsx
new file mode 100644
index 00000000..f12ed267
--- /dev/null
+++ b/src/components/Auth/resetPassword/AuthResetPassword.tsx
@@ -0,0 +1,111 @@
+import {
+ AuthCommonFieldEmail,
+ AuthCommonFieldNewPassword,
+ AuthCommonFieldOrganizationURL,
+ AuthCommonSection,
+ AuthCommonSubmitButton,
+ useAuthResetPasswordFlow,
+} from "@/components/Auth";
+import { Form } from "@/components/ui/form";
+import { useEffect } from "react";
+import { useLocation, useSearchParams, Link } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+import { LucideArrowLeft } from "lucide-react";
+
+export function AuthResetPassword() {
+ const [searchParams] = useSearchParams();
+ const location = useLocation();
+
+ const token = searchParams.get("token") || "";
+
+ const stateData = location.state as
+ | { tenant?: string; email?: string }
+ | undefined;
+ const tenantFromState = stateData?.tenant;
+ const emailFromState = stateData?.email;
+
+ const {
+ form,
+ handleSubmit,
+ handleError,
+ formError,
+ isLoading,
+ submittedWithErrors,
+ newPasswordRef,
+ tenantInputRef,
+ emailInputRef,
+ } = useAuthResetPasswordFlow({
+ defaultTenant: tenantFromState || "",
+ defaultEmail: emailFromState || "",
+ token,
+ });
+
+ useEffect(
+ function setInitialFormFieldFocus() {
+ if (!tenantFromState && !emailFromState)
+ return tenantInputRef.current?.focus();
+ newPasswordRef.current?.focus();
+ },
+ [tenantFromState, emailFromState, newPasswordRef, tenantInputRef]
+ );
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/Auth/resetPassword/useAuthResetPasswordFlow.ts b/src/components/Auth/resetPassword/useAuthResetPasswordFlow.ts
new file mode 100644
index 00000000..6f7eb0ab
--- /dev/null
+++ b/src/components/Auth/resetPassword/useAuthResetPasswordFlow.ts
@@ -0,0 +1,92 @@
+import { authCommonZodSchemas, createAuthError } from "@/components/Auth";
+import useResetPassword from "@/services/forgotPassword/mutations/useResetPassword";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { AxiosError } from "axios";
+import { useCallback, useRef, useState } from "react";
+import { useForm } from "react-hook-form";
+import { useNavigate } from "react-router-dom";
+import { toast } from "sonner";
+import { z } from "zod";
+
+const ResetPasswordSchema = z.object({
+ tenant: authCommonZodSchemas.organizationURL,
+ email: authCommonZodSchemas.email,
+ token: z.string(),
+ password: authCommonZodSchemas.newPassword,
+});
+
+type ResetPasswordData = z.infer;
+type ResetPasswordField = keyof ResetPasswordData;
+
+interface UseResetPasswordFlowProps {
+ defaultTenant?: string;
+ defaultEmail?: string;
+ token?: string;
+}
+
+export function useAuthResetPasswordFlow({
+ defaultTenant = "",
+ defaultEmail = "",
+ token = ""
+}: UseResetPasswordFlowProps = {}) {
+ const navigate = useNavigate();
+ const [formError, setFormError] = useState("");
+ const [submittedWithErrors, setSubmittedWithErrors] = useState(false);
+
+ const newPasswordRef = useRef(null);
+ const tenantInputRef = useRef(null);
+ const emailInputRef = useRef(null);
+
+ const form = useForm({
+ resolver: zodResolver(ResetPasswordSchema),
+ defaultValues: {
+ tenant: defaultTenant,
+ email: defaultEmail,
+ token: token,
+ password: "",
+ },
+ });
+
+ const { mutate: resetPassword, isPending: isLoading } = useResetPassword({
+ onSuccess: () => {
+ toast.success('Password updated successfully', { description: 'Please log in' });
+ navigate('/login');
+ },
+ onError: (error: AxiosError) => {
+ const authError = createAuthError(error);
+
+ if (authError.type === 'validation' && authError.field) {
+ form.setError(authError.field as ResetPasswordField, {
+ type: "manual",
+ message: authError.message
+ });
+ return;
+ }
+
+ setFormError(authError.message);
+ }
+ });
+
+ const handleSubmit = useCallback((data: ResetPasswordData) => {
+ setSubmittedWithErrors(false);
+ setFormError("");
+ resetPassword(data);
+ }, [resetPassword]);
+
+ const handleError = useCallback(() => {
+ setSubmittedWithErrors(true);
+ setFormError("Please check the form for errors");
+ }, []);
+
+ return {
+ form,
+ handleSubmit,
+ handleError,
+ formError,
+ isLoading,
+ submittedWithErrors,
+ newPasswordRef,
+ tenantInputRef,
+ emailInputRef
+ };
+}
\ No newline at end of file
diff --git a/src/components/Auth/signup/AuthSignup.tsx b/src/components/Auth/signup/AuthSignup.tsx
new file mode 100644
index 00000000..dcf4d69e
--- /dev/null
+++ b/src/components/Auth/signup/AuthSignup.tsx
@@ -0,0 +1,12 @@
+import { AuthCommonSection, AuthSignupForm } from "@/components/Auth";
+export function AuthSignup() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Auth/signup/AuthSignupForm.tsx b/src/components/Auth/signup/AuthSignupForm.tsx
new file mode 100644
index 00000000..e4f0c897
--- /dev/null
+++ b/src/components/Auth/signup/AuthSignupForm.tsx
@@ -0,0 +1,137 @@
+import {
+ authCommonZodSchemas,
+ AuthSignupStepCredentials,
+ AuthSignupStepOrganizationInfo,
+ useAuthSignupFlow,
+} from "@/components/Auth";
+import { Form } from "@/components/ui/form";
+import { cn } from "@/lib/utils";
+import { Step, defineStepper } from "@stepperize/react";
+import { createContext, FC, useContext } from "react";
+import { Link } from "react-router-dom";
+import { UseFormReturn } from "react-hook-form";
+import { z } from "zod";
+
+const OrganizationInfoSchema = z.object({
+ organizationName: authCommonZodSchemas.organizationName,
+ name: authCommonZodSchemas.name,
+ organizationURL: authCommonZodSchemas.organizationURL,
+});
+const CredentialsSchema = z.object({
+ email: authCommonZodSchemas.email,
+ password: authCommonZodSchemas.newPassword,
+});
+
+const step1 = {
+ id: "organizationInfo",
+ label: "Organization Info",
+ schema: OrganizationInfoSchema,
+};
+const step2 = {
+ id: "credentials",
+ label: "Credentials",
+ schema: CredentialsSchema,
+};
+const { Scoped } = defineStepper(step1, step2);
+
+type SignupFormShape = z.infer &
+ z.infer;
+
+interface AuthSignupFormContextType {
+ form: UseFormReturn;
+ currentStep: Step;
+ stepperMethods: {
+ prev: () => void;
+ next: () => void;
+ switch: (cases: Record T>) => T;
+ current: Step;
+ isLast: boolean;
+ isFirst: boolean;
+ };
+ isProcessing: boolean;
+ handleAttemptSubmit: (data: SignupFormShape) => void;
+ handleGoBack: () => void;
+ formError: string | null;
+}
+
+const AuthSignupFormContext = createContext<
+ AuthSignupFormContextType | undefined
+>(undefined);
+
+export function useAuthSignupFormContext() {
+ const context = useContext(AuthSignupFormContext);
+ if (!context) {
+ throw new Error(
+ "useAuthSignupFormContext must be used within an AuthSignupForm provider"
+ );
+ }
+ return context;
+}
+
+const AuthSignupFormContent: FC = () => {
+ const { form, stepperMethods, handleAttemptSubmit, formError } =
+ useAuthSignupFormContext();
+
+ return (
+ <>
+
+
+
+
+ Already a member of an Organization?{" "}
+
+ Log in
+
+
+ >
+ );
+};
+
+export function AuthSignupForm() {
+ const {
+ form,
+ stepperMethods,
+ handleAttemptSubmit,
+ handleGoBack,
+ formError,
+ isLoadingCoreFlow,
+ } = useAuthSignupFlow();
+
+ const currentStep = stepperMethods.current;
+ const isProcessing = isLoadingCoreFlow;
+
+ const contextValue = {
+ form,
+ currentStep,
+ stepperMethods,
+ isProcessing,
+ handleAttemptSubmit,
+ handleGoBack,
+ formError,
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/Auth/signup/AuthSignupStepCredentials.tsx b/src/components/Auth/signup/AuthSignupStepCredentials.tsx
new file mode 100644
index 00000000..99b5a1ca
--- /dev/null
+++ b/src/components/Auth/signup/AuthSignupStepCredentials.tsx
@@ -0,0 +1,53 @@
+import {
+ AuthCommonFieldEmail,
+ AuthCommonFieldNewPassword,
+ AuthCommonSubmitButton,
+ useAuthSignupFormContext,
+} from "@/components/Auth";
+import { Button } from "@/components/ui/button";
+import { LucideArrowLeft } from "lucide-react";
+import { useRef } from "react";
+import { useFormContext } from "react-hook-form";
+
+export function AuthSignupStepCredentials() {
+ const { control } = useFormContext();
+ const { isProcessing, handleGoBack } = useAuthSignupFormContext();
+ const emailRef = useRef(null);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/Auth/signup/AuthSignupStepOrganizationInfo.tsx b/src/components/Auth/signup/AuthSignupStepOrganizationInfo.tsx
new file mode 100644
index 00000000..1ffb5561
--- /dev/null
+++ b/src/components/Auth/signup/AuthSignupStepOrganizationInfo.tsx
@@ -0,0 +1,102 @@
+import {
+ AuthCommonFieldOrganizationURL,
+ AuthCommonSubmitButton,
+ useAuthSignupFormContext,
+} from "@/components/Auth";
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { createSlug } from "@/utils/slugify";
+import { useRef, useState } from "react";
+import { useFormContext } from "react-hook-form";
+
+export function AuthSignupStepOrganizationInfo() {
+ const { control, setValue } = useFormContext();
+ const { isProcessing } = useAuthSignupFormContext();
+ const orgURLRef = useRef(null);
+ const [isURLManuallyEdited, setIsURLManuallyEdited] = useState(false);
+
+ const handleOrganizationNameChange = (
+ e: React.ChangeEvent
+ ) => {
+ const value = e.target.value;
+ setValue("organizationName", value);
+ if (!isURLManuallyEdited) {
+ setValue("organizationURL", createSlug(value));
+ }
+ };
+
+ const handleOrganizationURLFocus = () => {
+ setIsURLManuallyEdited(true);
+ };
+
+ return (
+ <>
+ (
+
+ Your Full Name
+
+
+
+
+
+ )}
+ />
+ (
+
+ Organization Name
+
+ {
+ handleOrganizationNameChange(e);
+ field.onChange(e);
+ }}
+ />
+
+
+
+ )}
+ />
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/Auth/signup/useAuthSignupFlow.ts b/src/components/Auth/signup/useAuthSignupFlow.ts
new file mode 100644
index 00000000..cf83606f
--- /dev/null
+++ b/src/components/Auth/signup/useAuthSignupFlow.ts
@@ -0,0 +1,214 @@
+import { trackSignedIn, trackSignupStepCompleted, trackSignupStepMovedBack } from "@/analytics";
+import { AUTH_ERROR_MESSAGES, authCommonZodSchemas, createAuthError } from "@/components/Auth";
+import { LoginAndIdentifyParams, SignupPayload } from "@/services/auth/Auth.interfaces";
+import type { LoginResult } from "@/services/auth/mutations/useLoginAndIdentifyUser";
+import useLoginAndIdentifyUser from "@/services/auth/mutations/useLoginAndIdentifyUser";
+import useSignup from "@/services/auth/mutations/useSignup";
+import { isTenantRegistered, isTenantAvailable, useGetTenantByName } from "@/services/tenants/queries/useGetTenantByName";
+import { ApiHttpError } from "@/types";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { defineStepper, Step } from "@stepperize/react";
+import { AxiosError } from "axios";
+import { useCallback, useEffect, useState } from "react";
+import useSignIn from "react-auth-kit/hooks/useSignIn";
+import { useForm } from "react-hook-form";
+import { useNavigate } from "react-router-dom";
+import { toast } from "sonner";
+import { z } from "zod";
+
+const MAX_LOGIN_RETRIES = 4;
+
+const OrganizationInfoSchema = z.object({
+ organizationName: authCommonZodSchemas.organizationName,
+ name: authCommonZodSchemas.name,
+ organizationURL: authCommonZodSchemas.organizationURL,
+});
+
+const CredentialsSchema = z.object({
+ email: authCommonZodSchemas.email,
+ password: authCommonZodSchemas.newPassword,
+});
+
+type OrgInfoData = z.infer;
+type CredentialsData = z.infer;
+type SignupData = OrgInfoData & CredentialsData;
+
+const signupSteps = [
+ { id: "organizationInfo", label: "Organization Info", schema: OrganizationInfoSchema },
+ { id: "credentials", label: "Credentials", schema: CredentialsSchema }
+];
+
+const { useStepper } = defineStepper(...signupSteps);
+
+export function useAuthSignupFlow() {
+ const [formError, setFormError] = useState(null);
+ const [shouldCheckTenant, setShouldCheckTenant] = useState(false);
+ const [tenantNameToCheckQuery, setTenantNameToCheckQuery] = useState("");
+ const authKitSignIn = useSignIn();
+ const navigate = useNavigate();
+ const stepperMethods = useStepper();
+ const { next, prev, isLast, current: currentStep, all: steps } = stepperMethods;
+
+ const form = useForm({
+ resolver: zodResolver(currentStep.schema),
+ defaultValues: {
+ organizationName: "",
+ name: "",
+ organizationURL: "",
+ email: "",
+ password: "",
+ },
+ });
+
+ const {
+ data: tenantCheckResult,
+ error: tenantError,
+ status: tenantStatus,
+ isFetching: isCheckingTenant
+ } = useGetTenantByName(
+ tenantNameToCheckQuery,
+ {
+ enabled: shouldCheckTenant && !!tenantNameToCheckQuery,
+ throwOnError: false
+ }
+ );
+
+ const { mutate: loginAndIdentifyUser, isPending: isLoginPending } = useLoginAndIdentifyUser({
+ onSuccess: (data: LoginResult, variables: LoginAndIdentifyParams) => {
+ if (data.isSignedIn) {
+ trackSignedIn(variables.tenantSlug);
+ navigate('/');
+ } else {
+ toast.success('Organization created, but login failed. Please try logging in manually.');
+ navigate('/login');
+ }
+ },
+ retry: (failureCount) => {
+ if (failureCount < MAX_LOGIN_RETRIES) return true;
+
+ toast.success('Organization created successfully', {
+ description: 'You can now log in with your credentials',
+ });
+ navigate('/login');
+ return false;
+ }
+ });
+
+ const { mutate: signup, isPending: isSignupPending } = useSignup({
+ onError: (error: AxiosError) => {
+ const authError = createAuthError(error);
+
+ if (authError.type === 'validation' && authError.field) {
+ if (authError.field === 'password') {
+ form.setError('password', {
+ type: "manual",
+ message: authError.message
+ });
+ setTimeout(() => { form.setFocus('password'); }, 0);
+ return;
+ }
+ }
+
+ setFormError(authError.message);
+ },
+ onSuccess: (_, variables: SignupPayload) => {
+ trackSignupStepCompleted(steps.findIndex((s: Step) => s.id === currentStep.id) + 1, variables.tenant);
+ loginAndIdentifyUser({
+ email: variables.email,
+ password: variables.password,
+ tenantSlug: variables.tenant,
+ name: variables.name,
+ authKitSignIn,
+ });
+ },
+ });
+
+ const handleStepChange = useCallback(() => {
+ setFormError(null);
+ form.reset(form.getValues(), {
+ keepValues: true,
+ keepDirty: true,
+ keepErrors: false,
+ keepTouched: false,
+ keepIsSubmitted: false,
+ keepIsValid: false,
+ keepSubmitCount: false
+ });
+ }, [form]);
+
+ useEffect(() => {
+ handleStepChange();
+ }, [handleStepChange]);
+
+ useEffect(() => {
+ if (!shouldCheckTenant || isCheckingTenant) {
+ return;
+ }
+
+ if (tenantStatus === 'success') {
+ if (isTenantRegistered(tenantCheckResult)) {
+ form.setError("organizationURL", {
+ type: "manual",
+ message: AUTH_ERROR_MESSAGES.TENANT_TAKEN
+ });
+ setTimeout(() => { form.setFocus("organizationURL"); }, 0);
+ } else if (isTenantAvailable(tenantCheckResult)) {
+ trackSignupStepCompleted(steps.findIndex((s: Step) => s.id === currentStep.id) + 1, form.getValues("organizationName"));
+ next();
+ }
+ } else if (tenantStatus === 'error') {
+ const authError = createAuthError(tenantError);
+ setFormError(authError.message);
+ }
+
+ setShouldCheckTenant(false);
+
+ }, [tenantStatus, tenantCheckResult, tenantError, shouldCheckTenant, steps, currentStep.id, next, form, tenantNameToCheckQuery, isCheckingTenant]);
+
+ const handleAttemptSubmit = useCallback(async (data: Partial) => {
+ setFormError(null);
+
+ if (currentStep.id === 'organizationInfo') {
+ const tenantName = form.getValues("organizationURL");
+
+ if (!tenantName) {
+ form.trigger("organizationURL");
+ return;
+ }
+
+ setTenantNameToCheckQuery(tenantName);
+ setShouldCheckTenant(true);
+
+ } else if (currentStep.id === 'credentials' && isLast) {
+ const formData = { ...form.getValues(), ...data };
+
+ if (!formData.email || !formData.password || !formData.name || !formData.organizationName || !formData.organizationURL) {
+ setFormError(AUTH_ERROR_MESSAGES.MISSING_REQUIRED_FIELDS);
+ return;
+ }
+
+ signup({
+ email: formData.email,
+ name: formData.name,
+ password: formData.password,
+ tenant: formData.organizationURL
+ });
+ }
+ }, [currentStep.id, isLast, form, signup]);
+
+ const handleGoBack = useCallback(() => {
+ prev();
+ trackSignupStepMovedBack();
+ }, [prev]);
+
+ const isLoadingCoreFlow = isLoginPending || isSignupPending || isCheckingTenant;
+
+ return {
+ form,
+ stepperMethods,
+ handleAttemptSubmit,
+ handleGoBack,
+ formError,
+ isLoadingCoreFlow,
+ };
+}
\ No newline at end of file
diff --git a/src/components/NavigateWithOrg.tsx b/src/components/NavigateWithOrg.tsx
index b440e14d..1b3be5f8 100644
--- a/src/components/NavigateWithOrg.tsx
+++ b/src/components/NavigateWithOrg.tsx
@@ -3,55 +3,49 @@ import { Navigate, NavigateProps } from 'react-router-dom';
import useIsAuthenticated from 'react-auth-kit/hooks/useIsAuthenticated';
import useAuthUser from 'react-auth-kit/hooks/useAuthUser';
import { IUserData } from '@/types';
-import Auth from '@/components/Auth';
interface NavigateWithOrgProps extends NavigateProps {
children?: React.ReactNode;
to: string;
- fallbackToLogin?: boolean;
- fallbackToSignup?: boolean;
replace?: boolean;
}
-const NavigateWithOrg: React.FC = ({ children, to, fallbackToLogin = false, fallbackToSignup = false, replace = false, ...navigateProps }) => {
+/**
+ * Redirects authenticated users to their org-specific path.
+ * Renders children if the user is not authenticated (though this component
+ * is typically used within routes already protected by authentication checks).
+ */
+const NavigateWithOrg: React.FC = ({ children, to, replace = false, ...navigateProps }) => {
const isAuthenticated = useIsAuthenticated();
const authUser = useAuthUser();
const [redirectTo, setRedirectTo] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
+ let targetPath: string | null = null;
if (isAuthenticated) {
const user = authUser;
const organizationURL = user?.tenant;
if (organizationURL) {
- setRedirectTo(`/${organizationURL}${to}`);
+ targetPath = `/${organizationURL}${to.startsWith('/') ? to : `/${to}`}`;
} else {
- console.warn('Tenant information is missing.');
- setRedirectTo(null);
+ console.error('NavigateWithOrg: User is authenticated but tenant information is missing.');
}
- } else {
- setRedirectTo(null);
}
+
+ setRedirectTo(targetPath);
setLoading(false);
}, [isAuthenticated, authUser, to]);
if (loading) {
- return null; // TODO: Use a proper loader or nah?
+ return null;
}
if (redirectTo) {
return ;
}
- if (!isAuthenticated && fallbackToLogin) {
- return ;
- }
-
- if (!isAuthenticated && fallbackToSignup) {
- return ;
- }
-
- return <>{children}>; // Render children if no redirection is needed
+ return <>{children}>;
};
export default NavigateWithOrg;
\ No newline at end of file
diff --git a/src/components/Settings/UserDialogForm.tsx b/src/components/Settings/UserDialogForm.tsx
index 7aa3eb9d..e42aa64f 100644
--- a/src/components/Settings/UserDialogForm.tsx
+++ b/src/components/Settings/UserDialogForm.tsx
@@ -21,7 +21,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { MultiSelect } from "@/components/ui/MultiSelect";
-import NewPasswordField from "../Auth/fields/NewPasswordField";
+import InputPassword from "@/components/ui/InputPassword";
const getSchema = (selectedUserId: string | null) => {
return z.object({
@@ -102,9 +102,27 @@ const UserDialogForm = ({ selectedUserId, userForm, onSubmit, onCancel }: {
)}
/>
- {!selectedUserId &&
-
- }
+ {!selectedUserId && (
+ (
+
+ Password
+
+
+
+
+
+ )}
+ />
+ )}
{Boolean(selectedUserId) &&
, 'type'> {
+ /**
+ * Enable strong password validation with visual feedback
+ */
+ newPasswordChecks?: boolean;
+ /**
+ * Whether the form has been submitted with errors
+ */
+ showValidationErrors?: boolean;
+}
+
+const InputPassword = forwardRef(({
+ newPasswordChecks,
+ showValidationErrors = false,
+ className = "",
+ value = "",
+ onChange,
+ ...props
+}, ref) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [validations, setValidations] = useState({
+ hasUppercase: regexIsUppercase.test(value as string),
+ hasNumber: regexIsNumber.test(value as string),
+ hasSpecialChar: regexIsSpecialCharacter.test(value as string),
+ meetsMinLength: (value as string)?.length >= passwordMinLengthValue
+ });
+
+ useEffect(() => {
+ if (newPasswordChecks) {
+ setValidations({
+ hasUppercase: regexIsUppercase.test(value as string),
+ hasNumber: regexIsNumber.test(value as string),
+ hasSpecialChar: regexIsSpecialCharacter.test(value as string),
+ meetsMinLength: (value as string)?.length >= passwordMinLengthValue
+ });
+ }
+ }, [value, newPasswordChecks]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ onChange?.(e);
+ };
+
+ return (
+
+
+
+
+
+
+ {newPasswordChecks && (
+
+ )}
+
+ );
+});
+
+interface ValidationRequirementProps {
+ isValid: boolean;
+ showError: boolean;
+ text: string;
+}
+
+function ValidationRequirement({ isValid, showError, text }: ValidationRequirementProps) {
+ return (
+
+ {isValid ? (
+
+ ) : (
+
+ )}
+ {text}
+
+ );
+}
+
+InputPassword.displayName = "InputPassword";
+
+export default InputPassword;
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index 07c1bb17..32a4e3b5 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,72 +1,78 @@
import ListAgents from "@/components/agents/ListAgents";
import AppPageHeader from "@/components/AppPageHeader";
-import ForgotPassword from "@/components/Auth/ForgotPassword";
-import ResetPassword from "@/components/Auth/ResetPassword";
+import AuditWrapper from "@/components/audit/AuditWrapper";
+import {
+ AuthForgotPassword,
+ AuthLayout,
+ AuthLogin,
+ AuthRedirectGuard,
+ AuthResetPassword,
+ AuthSignup,
+} from "@/components/Auth";
import AxiosInterceptor from "@/components/AxiosInterceptor";
+import BodyPortal from "@/components/BodyPortal";
import NavigateWithOrg from "@/components/NavigateWithOrg";
import { NotFound } from "@/components/NotFound";
import RequireActiveUser from "@/components/RequireActiveUser";
+import ListRotators from "@/components/rotators/ListRotators";
import Settings from "@/components/Settings";
import { ThemeProvider } from "@/components/ThemeProvider";
+import { Loader } from "@/components/ui/Loader";
import { Toaster } from "@/components/ui/sonner";
+import { TooltipProvider } from "@/components/ui/tooltip";
import { Verify } from "@/components/Verify";
-import authStore from "@/services/auth/authStore";
-import RequireAuth from '@auth-kit/react-router/RequireAuth';
-import { useEffect, StrictMode, Suspense } from 'react';
-import AuthProvider from 'react-auth-kit';
-import { createRoot } from 'react-dom/client';
-import { createBrowserRouter, RouterProvider } from 'react-router-dom';
-import { load, page } from './analytics';
-import App from './App';
-import './index.css';
import { DOCS_DOMAIN, IS_PROD } from "@/constants";
-import ListRotators from "@/components/rotators/ListRotators";
+import { FeatureFlagProvider } from "@/context/FeatureFlagContext";
+import { LayoutProvider } from "@/context/LayoutContext";
+import { SubscriptionProvider } from "@/context/SubscriptionContext";
+import authStore from "@/services/auth/authStore";
+import RequireAuth from "@auth-kit/react-router/RequireAuth";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
-import AuditWrapper from "@/components/audit/AuditWrapper";
+import { StrictMode, Suspense, useEffect } from "react";
+import AuthProvider from "react-auth-kit";
+import { createRoot } from "react-dom/client";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import { load, page } from "./analytics";
+import App from "./App";
import OrgRedirector from "./components/OrgRedirector";
-import { Loader } from "@/components/ui/Loader";
-import { SubscriptionProvider } from '@/context/SubscriptionContext';
-import { FeatureFlagProvider } from '@/context/FeatureFlagContext';
-import { TooltipProvider } from "@/components/ui/tooltip";
-import { LayoutProvider } from '@/context/LayoutContext';
-import BodyPortal from '@/components/BodyPortal';
+import "./index.css";
-const queryClient = new QueryClient()
+const queryClient = new QueryClient();
const router = createBrowserRouter([
{
- path: '/',
+ path: "/",
element: (
-
+
),
},
{
path: "/verify",
- element:
-
-
- },
- {
- path: '/signup',
- element: ,
- },
- {
- path: '/login',
- element: ,
- },
- {
- path: '/forgot-password',
- element: ,
+ element: (
+
+
+
+ ),
},
{
- path: '/reset-password',
- element: ,
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ { path: "/signup", element: },
+ { path: "/login", element: },
+ ],
+ },
+ { path: "/forgot-password", element: },
+ { path: "/reset-password", element: },
+ ],
},
{
- path: '/:org',
+ path: "/:org",
element: (
@@ -82,11 +88,11 @@ const router = createBrowserRouter([
),
children: [
{
- path: '',
+ path: "",
element: ,
},
{
- path: 'agents',
+ path: "agents",
element: (
<>
>
- )
+ ),
},
{
- path: 'rotators',
+ path: "rotators",
element: (
<>
>
- )
+ ),
},
// TODO: Remove mock variable when audit is ready https://github.com/external-secrets-inc/web-ui/issues/124
import.meta.env.VITE_MOCK_AUDIT_ROUTE ? {
@@ -140,7 +146,7 @@ const router = createBrowserRouter([
)
} : {},
{
- path: 'settings',
+ path: "settings",
element: (
<>
>
- )
+ ),
},
],
},
{
- path: '*',
+ path: "*",
element: ,
},
]);
@@ -180,7 +186,7 @@ const Main = () => {
return ;
};
-const rootElement = document.getElementById('root');
+const rootElement = document.getElementById("root");
if (rootElement) {
createRoot(rootElement).render(
diff --git a/src/services/auth/mutations/useLoginAndIdentifyUser.ts b/src/services/auth/mutations/useLoginAndIdentifyUser.ts
index 20e7b848..cb61e03f 100644
--- a/src/services/auth/mutations/useLoginAndIdentifyUser.ts
+++ b/src/services/auth/mutations/useLoginAndIdentifyUser.ts
@@ -1,53 +1,63 @@
import { LoginAndIdentifyParams } from "@/services/auth/Auth.interfaces";
import { performLogin } from "@/services/auth/mutations/usePerformLogin";
import { getUserData } from "@/services/users/queries/useGetUserData";
-import { ApiHttpError } from "@/types";
+import { ApiHttpError, IUserData } from "@/types";
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
import { AxiosError } from "axios";
+export interface LoginResult {
+ isSignedIn: boolean;
+ userState: IUserData;
+}
+
const buildUserState = ({
email,
name,
userDetails,
tenantId,
- tenant,
+ tenantSlug,
userId,
}: {
email: string;
name?: string;
userDetails: Awaited>;
tenantId: string;
- tenant: unknown;
+ tenantSlug: string;
userId: string;
-}) => {
- return {
- email,
- name: name || userDetails.name,
- isActive: userDetails.is_active,
- tenantId,
- tenant,
- userId,
- };
-};
+}): IUserData => ({
+ email,
+ name: name || userDetails.name,
+ isActive: userDetails.is_active,
+ tenantId,
+ tenant: tenantSlug,
+ organizationURL: tenantSlug,
+ userId,
+});
const signInUser = (
authKitSignIn: LoginAndIdentifyParams["authKitSignIn"],
token: string,
- userState: Record
-) => {
- return authKitSignIn({
- auth: {
- token,
- type: "Bearer",
- },
- userState,
- });
-};
+ userState: IUserData
+): boolean => authKitSignIn({
+ auth: {
+ token,
+ type: "Bearer",
+ },
+ userState,
+});
-const identifyUserWithAnalytics = (userState: Record) => {
- const { userId, email, name, ...segmentUserState } = userState; // eslint-disable-line @typescript-eslint/no-unused-vars
+const identifyUserWithAnalytics = (userState: IUserData): void => {
+ const { userId, organizationURL, tenantId } = userState;
+ const traits = {
+ organizationURL,
+ tenantId,
+ };
try {
- analytics.identify(userId as string, segmentUserState);
+ if (typeof analytics !== 'undefined' && analytics.identify) {
+ analytics.identify(userId, traits);
+ } else {
+ console.warn("Segment analytics.identify not available.");
+ }
} catch (error) {
console.error("Segment identify call failed:", error);
}
@@ -59,39 +69,38 @@ const loginAndIdentifyUser = async ({
tenantSlug,
name,
authKitSignIn,
-}: LoginAndIdentifyParams): Promise => {
- const { token, tenantId, tenant, userId } = await performLogin({email, password, tenant: tenantSlug});
- const userDetails = await getUserData(userId!, token);
-
+}: LoginAndIdentifyParams): Promise => {
+ const { token, tenantId, tenant: returnedTenantSlug, userId } = await performLogin({ email, password, tenant: tenantSlug });
+
+ if (!userId) {
+ throw new Error("Login failed: No user ID returned");
+ }
+
+ const userDetails = await getUserData(userId, token);
const userState = buildUserState({
email,
name,
userDetails,
tenantId,
- tenant,
- userId: userId!,
+ tenantSlug: returnedTenantSlug,
+ userId,
});
-
+
const isSignedIn = signInUser(authKitSignIn, token, userState);
-
+
if (isSignedIn) {
identifyUserWithAnalytics(userState);
- return true;
}
-
- return false;
-};
+ return { isSignedIn, userState };
+};
const useLoginAndIdentifyUser = (
- options?: Omit, LoginAndIdentifyParams>, 'mutationKey' | 'mutationFn'>
-) => {
-
- return useMutation({
- mutationKey: ["useLoginAndIdentifyUser"],
- mutationFn: (variables: LoginAndIdentifyParams) => loginAndIdentifyUser(variables),
- ...options,
- });
-};
+ options?: Omit, LoginAndIdentifyParams>, 'mutationKey' | 'mutationFn'>
+) => useMutation({
+ mutationKey: ["auth", "useLoginAndIdentifyUser"],
+ mutationFn: loginAndIdentifyUser,
+ ...options,
+});
export default useLoginAndIdentifyUser;
diff --git a/src/services/tenants/queries/useGetTenantByName.ts b/src/services/tenants/queries/useGetTenantByName.ts
new file mode 100644
index 00000000..307dddb5
--- /dev/null
+++ b/src/services/tenants/queries/useGetTenantByName.ts
@@ -0,0 +1,53 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+import axiosInstance from "@/services/axiosConfig";
+import { isAxiosError } from "axios";
+import { ApiHttpError } from "@/types";
+
+export interface TenantDetails {
+ name: string;
+}
+
+export interface TenantValidationResult {
+ exists: boolean;
+ details?: TenantDetails;
+}
+
+const validateTenantName = async (signal: AbortSignal, tenantName: string): Promise => {
+ try {
+ const response = await axiosInstance.get(
+ `/public/tenants/${encodeURIComponent(tenantName)}`,
+ { signal }
+ );
+
+ return {
+ exists: true,
+ details: response.data
+ };
+ } catch (error) {
+ if (isAxiosError(error) && error.response?.status === 404) {
+ return { exists: false };
+ }
+ throw error;
+ }
+};
+
+export const useGetTenantByName = (
+ tenantName: string,
+ options?: Omit, 'queryKey' | 'queryFn'>
+) => {
+ return useQuery({
+ queryKey: ['auth', 'useGetTenantByName', tenantName] as const,
+ queryFn: ({ signal }) => validateTenantName(signal, tenantName),
+ retry: (failureCount) => failureCount < 3,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ ...options,
+ });
+};
+
+export const isTenantRegistered = (result: TenantValidationResult | undefined): boolean => {
+ return !!result?.exists;
+};
+
+export const isTenantAvailable = (result: TenantValidationResult | undefined): boolean => {
+ return result !== undefined && !result.exists;
+};
\ No newline at end of file