mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
fix(auth): cookie expiry and renewal (#1527)
* fix(auth/web): logout when expired/invalid/no cookie is present * fix(auth/web): specify error message in invalid cookie * fix(auth/web): reset error boundary on login * fix(auth/web): fix onboarding * chore: code cleanup * fix(web): revert tanstack/router to 1.31.0 * refactor(web): remove react-error-boundary * feat(auth): refresh cookie when close to expiry * enhancement(web): specify defaultError message in HttpClient * fix(web): use absolute paths for router links (#1530) * chore(web): bump `@tanstack/react-router` to `1.31.6` * fix(web): settings routes * fix(web): filter routes * fix(web): remove unused ReleasesIndexRoute * chore(web): add documentation for HttpClient * chore(lint): remove unnecessary whitespace
This commit is contained in:
parent
3dab295387
commit
8120c33f6b
19 changed files with 364 additions and 366 deletions
|
@ -7,6 +7,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/pkg/errors"
|
"github.com/autobrr/autobrr/pkg/errors"
|
||||||
|
@ -82,6 +83,7 @@ func (h authHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Set user as authenticated
|
// Set user as authenticated
|
||||||
session.Values["authenticated"] = true
|
session.Values["authenticated"] = true
|
||||||
|
session.Values["created"] = time.Now().Unix()
|
||||||
|
|
||||||
// Set cookie options
|
// Set cookie options
|
||||||
session.Options.HttpOnly = true
|
session.Options.HttpOnly = true
|
||||||
|
|
|
@ -57,6 +57,27 @@ func (s Server) IsAuthenticated(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if created, ok := session.Values["created"].(int64); ok {
|
||||||
|
// created is a unix timestamp MaxAge is in seconds
|
||||||
|
maxAge := time.Duration(session.Options.MaxAge) * time.Second
|
||||||
|
expires := time.Unix(created, 0).Add(maxAge)
|
||||||
|
|
||||||
|
if time.Until(expires) <= 7*24*time.Hour { // 7 days
|
||||||
|
s.log.Info().Msgf("Cookie is expiring in less than 7 days on %s - extending session", expires.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
session.Values["created"] = time.Now().Unix()
|
||||||
|
|
||||||
|
// Call session.Save as needed - since it writes a header (the Set-Cookie
|
||||||
|
// header), making sure you call it before writing out a body is important.
|
||||||
|
// https://github.com/gorilla/sessions/issues/178#issuecomment-447674812
|
||||||
|
if err := session.Save(r, w); err != nil {
|
||||||
|
s.log.Error().Err(err).Msgf("could not store session: %s", r.RemoteAddr)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), "session", session)
|
ctx := context.WithValue(r.Context(), "session", session)
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tanstack/react-query": "^5.29.2",
|
"@tanstack/react-query": "^5.29.2",
|
||||||
"@tanstack/react-query-devtools": "^5.29.2",
|
"@tanstack/react-query-devtools": "^5.29.2",
|
||||||
"@tanstack/react-router": "^1.28.5",
|
"@tanstack/react-router": "^1.31.6",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/react": "^18.2.79",
|
"@types/react": "^18.2.79",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
|
@ -58,7 +58,6 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-debounce-input": "^3.3.0",
|
"react-debounce-input": "^3.3.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.13",
|
|
||||||
"react-hook-form": "^7.51.3",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-multi-select-component": "^4.3.4",
|
"react-multi-select-component": "^4.3.4",
|
||||||
|
@ -78,7 +77,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
||||||
"@rollup/wasm-node": "^4.14.3",
|
"@rollup/wasm-node": "^4.14.3",
|
||||||
"@tanstack/router-devtools": "^1.28.5",
|
"@tanstack/router-devtools": "^1.31.6",
|
||||||
"@types/node": "^20.12.2",
|
"@types/node": "^20.12.2",
|
||||||
"@types/react": "^18.2.73",
|
"@types/react": "^18.2.73",
|
||||||
"@types/react-dom": "^18.2.23",
|
"@types/react-dom": "^18.2.23",
|
||||||
|
|
195
web/pnpm-lock.yaml
generated
195
web/pnpm-lock.yaml
generated
|
@ -35,8 +35,8 @@ importers:
|
||||||
specifier: ^5.29.2
|
specifier: ^5.29.2
|
||||||
version: 5.29.2(@tanstack/react-query@5.29.2(react@18.2.0))(react@18.2.0)
|
version: 5.29.2(@tanstack/react-query@5.29.2(react@18.2.0))(react@18.2.0)
|
||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.28.5
|
specifier: ^1.31.6
|
||||||
version: 1.28.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.31.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.12.7
|
specifier: ^20.12.7
|
||||||
version: 20.12.7
|
version: 20.12.7
|
||||||
|
@ -88,9 +88,6 @@ importers:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
react-error-boundary:
|
|
||||||
specifier: ^4.0.13
|
|
||||||
version: 4.0.13(react@18.2.0)
|
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.51.3
|
specifier: ^7.51.3
|
||||||
version: 7.51.3(react@18.2.0)
|
version: 7.51.3(react@18.2.0)
|
||||||
|
@ -144,8 +141,8 @@ importers:
|
||||||
specifier: ^4.14.3
|
specifier: ^4.14.3
|
||||||
version: 4.14.3
|
version: 4.14.3
|
||||||
'@tanstack/router-devtools':
|
'@tanstack/router-devtools':
|
||||||
specifier: ^1.28.5
|
specifier: ^1.31.6
|
||||||
version: 1.28.5(csstype@3.1.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.31.6(csstype@3.1.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
|
@ -178,7 +175,7 @@ importers:
|
||||||
version: 0.19.8(vite@5.2.9(@types/node@20.12.7)(terser@5.30.1))(workbox-build@7.0.0)(workbox-window@7.0.0)
|
version: 0.19.8(vite@5.2.9(@types/node@20.12.7)(terser@5.30.1))(workbox-build@7.0.0)(workbox-window@7.0.0)
|
||||||
vite-plugin-svgr:
|
vite-plugin-svgr:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0(@rollup/wasm-node@4.14.3)(typescript@5.4.5)(vite@5.2.9(@types/node@20.12.7)(terser@5.30.1))
|
version: 4.2.0(@rollup/wasm-node@4.17.2)(typescript@5.4.5)(vite@5.2.9(@types/node@20.12.7)(terser@5.30.1))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
@ -806,10 +803,6 @@ packages:
|
||||||
resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==}
|
resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/runtime@7.24.0':
|
|
||||||
resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/runtime@7.24.1':
|
'@babel/runtime@7.24.1':
|
||||||
resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==}
|
resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
@ -1170,6 +1163,11 @@ packages:
|
||||||
resolution: {integrity: sha512-UyFUQV/iAu/Wt6rY6uQMYBQlfTMsynzYVIz6i7s9ySwjoG9WDNgtkK1TrazCSrUFbmuPZi2gbJm6VWdJCVw2yA==}
|
resolution: {integrity: sha512-UyFUQV/iAu/Wt6rY6uQMYBQlfTMsynzYVIz6i7s9ySwjoG9WDNgtkK1TrazCSrUFbmuPZi2gbJm6VWdJCVw2yA==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@rollup/wasm-node@4.17.2':
|
||||||
|
resolution: {integrity: sha512-4F6C3XaUn02XY/GJMQTXncWrLyCkRHdRZe4OyWuQUprWKmU2u+esISOtCYdr3Bp9AqCIo/X3So2Ik7N9dNDwow==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||||
|
|
||||||
|
@ -1321,8 +1319,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
|
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
|
||||||
|
|
||||||
'@tanstack/history@1.26.10':
|
'@tanstack/history@1.28.9':
|
||||||
resolution: {integrity: sha512-fHx8RQ3liEDhueIemUggBGmqYnK6vOxtxCduolW7r6ExBEQVwKdLEcaUobxp6BxcXLQ7z/qhXAptlOlYi4FFXg==}
|
resolution: {integrity: sha512-WgTFJhHaZnGZPyt0H11xFhGGDj1MtA1mrUmdAjB/nhVpmsAYXsSB5O+hkF9N66u7MjbNb405wTb9diBsztvI5w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@tanstack/query-core@5.29.0':
|
'@tanstack/query-core@5.29.0':
|
||||||
|
@ -1342,8 +1340,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
|
||||||
'@tanstack/react-router@1.28.5':
|
'@tanstack/react-router@1.31.6':
|
||||||
resolution: {integrity: sha512-d6DHZ2Uw9Gd3Ry3k1GVg4Opi9PYV09CO9zCJKDWWzt07ts5UXPLKgK/nIm8t4gv6DR/VrhUcZsiN0kZnkOQXwg==}
|
resolution: {integrity: sha512-Al8IwmZQk0km4p/8KKI8dO6bytZfAnw7ukmP2PMSZX0fEFA3sd9gPbnqBZTA/dHdl4qTLPnbdKWUTz8D4BLoyA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
@ -1361,8 +1359,8 @@ packages:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
'@tanstack/router-devtools@1.28.5':
|
'@tanstack/router-devtools@1.31.6':
|
||||||
resolution: {integrity: sha512-6TGaDW3+RY681so0AAtXkzwQa7hlGrxCRSJCJt+8F8UAxak0fm1Dj4PWJTfGsYx5r/R2DpfUqWilj9KCGsOZKA==}
|
resolution: {integrity: sha512-vtlEk8uu0eiEjGQVN7okE0ly9XJQ1HZO8e2CwTm4nymUn1N+RRHarbVJcSn9Sh3Hynn7MyqrVquo7VDFT6bzGQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
@ -1374,17 +1372,6 @@ packages:
|
||||||
'@tanstack/virtual-core@3.0.0':
|
'@tanstack/virtual-core@3.0.0':
|
||||||
resolution: {integrity: sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==}
|
resolution: {integrity: sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==}
|
||||||
|
|
||||||
'@testing-library/dom@10.0.0':
|
|
||||||
resolution: {integrity: sha512-PmJPnogldqoVFf+EwbHvbBJ98MmqASV8kLrBYgsDNxQcFMeIS7JFL48sfyXvuMtgmWO/wMhh25odr+8VhDmn4g==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
'@testing-library/react@15.0.2':
|
|
||||||
resolution: {integrity: sha512-5mzIpuytB1ctpyywvyaY2TAAUQVCZIGqwiqFQf6u9lvj/SJQepGUzNV18Xpk+NLCaCE2j7CWrZE0tEf9xLZYiQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^18.2.0
|
|
||||||
react-dom: ^18.0.0
|
|
||||||
|
|
||||||
'@tsconfig/node10@1.0.9':
|
'@tsconfig/node10@1.0.9':
|
||||||
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
|
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
|
||||||
|
|
||||||
|
@ -1397,9 +1384,6 @@ packages:
|
||||||
'@tsconfig/node16@1.0.4':
|
'@tsconfig/node16@1.0.4':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
'@types/aria-query@5.0.4':
|
|
||||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
|
||||||
|
|
||||||
'@types/estree@0.0.39':
|
'@types/estree@0.0.39':
|
||||||
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||||
|
|
||||||
|
@ -1556,10 +1540,6 @@ packages:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
ansi-styles@5.2.0:
|
|
||||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
ansi-styles@6.2.1:
|
ansi-styles@6.2.1:
|
||||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -1580,9 +1560,6 @@ packages:
|
||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
aria-query@5.3.0:
|
|
||||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
|
||||||
|
|
||||||
array-buffer-byte-length@1.0.1:
|
array-buffer-byte-length@1.0.1:
|
||||||
resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
|
resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -1638,6 +1615,7 @@ packages:
|
||||||
autoprefixer@10.4.19:
|
autoprefixer@10.4.19:
|
||||||
resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
|
resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss: ^8.1.0
|
postcss: ^8.1.0
|
||||||
|
|
||||||
|
@ -1868,10 +1846,6 @@ packages:
|
||||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
dequal@2.0.3:
|
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
didyoumean@1.2.2:
|
didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
|
@ -1894,9 +1868,6 @@ packages:
|
||||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
dom-accessibility-api@0.5.16:
|
|
||||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
|
||||||
|
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
|
@ -2036,6 +2007,7 @@ packages:
|
||||||
eslint-watch@8.0.0:
|
eslint-watch@8.0.0:
|
||||||
resolution: {integrity: sha512-piws/uE4gkZdz1pwkaEFx+kSWvoGnVX8IegFRrE1NUvlXjtU0rg7KhT1QDj/NzhAwbiLEfdRHWz5q738R4zDKA==}
|
resolution: {integrity: sha512-piws/uE4gkZdz1pwkaEFx+kSWvoGnVX8IegFRrE1NUvlXjtU0rg7KhT1QDj/NzhAwbiLEfdRHWz5q738R4zDKA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=8 <9.0.0'
|
eslint: '>=8 <9.0.0'
|
||||||
|
|
||||||
|
@ -2599,9 +2571,6 @@ packages:
|
||||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
lz-string@1.5.0:
|
|
||||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
|
||||||
|
|
||||||
magic-string@0.25.9:
|
magic-string@0.25.9:
|
||||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||||
|
|
||||||
|
@ -2849,10 +2818,6 @@ packages:
|
||||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
pretty-format@27.5.1:
|
|
||||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
|
||||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
|
||||||
|
|
||||||
printable-characters@1.0.42:
|
printable-characters@1.0.42:
|
||||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||||
|
|
||||||
|
@ -2879,11 +2844,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
|
||||||
react-error-boundary@4.0.13:
|
|
||||||
resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^18.2.0
|
|
||||||
|
|
||||||
react-fast-compare@2.0.4:
|
react-fast-compare@2.0.4:
|
||||||
resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==}
|
resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==}
|
||||||
|
|
||||||
|
@ -2906,9 +2866,6 @@ packages:
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
react-is@17.0.2:
|
|
||||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
|
||||||
|
|
||||||
react-multi-select-component@4.3.4:
|
react-multi-select-component@4.3.4:
|
||||||
resolution: {integrity: sha512-Ui/bzCbROF4WfKq3OKWyQJHmy/bd1mW7CQM+L83TfiltuVvHElhKEyPM3JzO9urIcWplBUKv+kyxqmEnd9jPcA==}
|
resolution: {integrity: sha512-Ui/bzCbROF4WfKq3OKWyQJHmy/bd1mW7CQM+L83TfiltuVvHElhKEyPM3JzO9urIcWplBUKv+kyxqmEnd9jPcA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -3250,6 +3207,7 @@ packages:
|
||||||
|
|
||||||
ts-node@10.9.2:
|
ts-node@10.9.2:
|
||||||
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@swc/core': '>=1.2.50'
|
'@swc/core': '>=1.2.50'
|
||||||
'@swc/wasm': '>=1.2.50'
|
'@swc/wasm': '>=1.2.50'
|
||||||
|
@ -3335,6 +3293,7 @@ packages:
|
||||||
|
|
||||||
update-browserslist-db@1.0.13:
|
update-browserslist-db@1.0.13:
|
||||||
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
|
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
@ -3398,6 +3357,7 @@ packages:
|
||||||
vite@5.2.9:
|
vite@5.2.9:
|
||||||
resolution: {integrity: sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==}
|
resolution: {integrity: sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/node': ^18.0.0 || >=20.0.0
|
'@types/node': ^18.0.0 || >=20.0.0
|
||||||
less: '*'
|
less: '*'
|
||||||
|
@ -4316,10 +4276,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
|
|
||||||
'@babel/runtime@7.24.0':
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime: 0.14.1
|
|
||||||
|
|
||||||
'@babel/runtime@7.24.1':
|
'@babel/runtime@7.24.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
|
@ -4636,43 +4592,43 @@ snapshots:
|
||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
'@rollup/plugin-babel@5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.14.3)':
|
'@rollup/plugin-babel@5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.17.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.24.3
|
'@babel/core': 7.24.3
|
||||||
'@babel/helper-module-imports': 7.24.3
|
'@babel/helper-module-imports': 7.24.3
|
||||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.3)
|
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.17.2)
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
|
|
||||||
'@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.14.3)':
|
'@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.17.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.3)
|
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.17.2)
|
||||||
'@types/resolve': 1.17.1
|
'@types/resolve': 1.17.1
|
||||||
builtin-modules: 3.3.0
|
builtin-modules: 3.3.0
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
is-module: 1.0.0
|
is-module: 1.0.0
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
|
|
||||||
'@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.14.3)':
|
'@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.17.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.3)
|
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.17.2)
|
||||||
magic-string: 0.25.9
|
magic-string: 0.25.9
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
|
|
||||||
'@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.14.3)':
|
'@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.17.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 0.0.39
|
'@types/estree': 0.0.39
|
||||||
estree-walker: 1.0.1
|
estree-walker: 1.0.1
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
|
|
||||||
'@rollup/pluginutils@5.1.0(@rollup/wasm-node@4.14.3)':
|
'@rollup/pluginutils@5.1.0(@rollup/wasm-node@4.17.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.5
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
|
|
||||||
'@rollup/wasm-node@4.14.3':
|
'@rollup/wasm-node@4.14.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4680,6 +4636,12 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
'@rollup/wasm-node@4.17.2':
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.5
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
ejs: 3.1.9
|
ejs: 3.1.9
|
||||||
|
@ -4812,7 +4774,7 @@ snapshots:
|
||||||
mini-svg-data-uri: 1.4.4
|
mini-svg-data-uri: 1.4.4
|
||||||
tailwindcss: 3.4.3(ts-node@10.9.2(@swc/core@1.4.2)(@types/node@20.12.7)(typescript@5.4.5))
|
tailwindcss: 3.4.3(ts-node@10.9.2(@swc/core@1.4.2)(@types/node@20.12.7)(typescript@5.4.5))
|
||||||
|
|
||||||
'@tanstack/history@1.26.10': {}
|
'@tanstack/history@1.28.9': {}
|
||||||
|
|
||||||
'@tanstack/query-core@5.29.0': {}
|
'@tanstack/query-core@5.29.0': {}
|
||||||
|
|
||||||
|
@ -4829,11 +4791,10 @@ snapshots:
|
||||||
'@tanstack/query-core': 5.29.0
|
'@tanstack/query-core': 5.29.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
'@tanstack/react-router@1.28.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@tanstack/react-router@1.31.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/history': 1.26.10
|
'@tanstack/history': 1.28.9
|
||||||
'@tanstack/react-store': 0.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@tanstack/react-store': 0.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@testing-library/react': 15.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
|
@ -4852,9 +4813,9 @@ snapshots:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
'@tanstack/router-devtools@1.28.5(csstype@3.1.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
'@tanstack/router-devtools@1.31.6(csstype@3.1.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tanstack/react-router': 1.28.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
'@tanstack/react-router': 1.31.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
clsx: 2.1.0
|
clsx: 2.1.0
|
||||||
date-fns: 2.30.0
|
date-fns: 2.30.0
|
||||||
goober: 2.1.14(csstype@3.1.2)
|
goober: 2.1.14(csstype@3.1.2)
|
||||||
|
@ -4867,25 +4828,6 @@ snapshots:
|
||||||
|
|
||||||
'@tanstack/virtual-core@3.0.0': {}
|
'@tanstack/virtual-core@3.0.0': {}
|
||||||
|
|
||||||
'@testing-library/dom@10.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.24.2
|
|
||||||
'@babel/runtime': 7.24.1
|
|
||||||
'@types/aria-query': 5.0.4
|
|
||||||
aria-query: 5.3.0
|
|
||||||
chalk: 4.1.2
|
|
||||||
dom-accessibility-api: 0.5.16
|
|
||||||
lz-string: 1.5.0
|
|
||||||
pretty-format: 27.5.1
|
|
||||||
|
|
||||||
'@testing-library/react@15.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.1
|
|
||||||
'@testing-library/dom': 10.0.0
|
|
||||||
'@types/react-dom': 18.2.25
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
|
|
||||||
'@tsconfig/node10@1.0.9': {}
|
'@tsconfig/node10@1.0.9': {}
|
||||||
|
|
||||||
'@tsconfig/node12@1.0.11': {}
|
'@tsconfig/node12@1.0.11': {}
|
||||||
|
@ -4894,8 +4836,6 @@ snapshots:
|
||||||
|
|
||||||
'@tsconfig/node16@1.0.4': {}
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
'@types/aria-query@5.0.4': {}
|
|
||||||
|
|
||||||
'@types/estree@0.0.39': {}
|
'@types/estree@0.0.39': {}
|
||||||
|
|
||||||
'@types/estree@1.0.5': {}
|
'@types/estree@1.0.5': {}
|
||||||
|
@ -5081,8 +5021,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
ansi-styles@5.2.0: {}
|
|
||||||
|
|
||||||
ansi-styles@6.2.1: {}
|
ansi-styles@6.2.1: {}
|
||||||
|
|
||||||
any-promise@1.3.0: {}
|
any-promise@1.3.0: {}
|
||||||
|
@ -5098,10 +5036,6 @@ snapshots:
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
aria-query@5.3.0:
|
|
||||||
dependencies:
|
|
||||||
dequal: 2.0.3
|
|
||||||
|
|
||||||
array-buffer-byte-length@1.0.1:
|
array-buffer-byte-length@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
|
@ -5438,8 +5372,6 @@ snapshots:
|
||||||
has-property-descriptors: 1.0.2
|
has-property-descriptors: 1.0.2
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
|
||||||
|
|
||||||
didyoumean@1.2.2: {}
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
diff@4.0.2: {}
|
diff@4.0.2: {}
|
||||||
|
@ -5458,8 +5390,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|
||||||
dom-accessibility-api@0.5.16: {}
|
|
||||||
|
|
||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.4
|
'@babel/runtime': 7.23.4
|
||||||
|
@ -6290,8 +6220,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
lz-string@1.5.0: {}
|
|
||||||
|
|
||||||
magic-string@0.25.9:
|
magic-string@0.25.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
sourcemap-codec: 1.4.8
|
sourcemap-codec: 1.4.8
|
||||||
|
@ -6522,12 +6450,6 @@ snapshots:
|
||||||
|
|
||||||
pretty-bytes@6.1.1: {}
|
pretty-bytes@6.1.1: {}
|
||||||
|
|
||||||
pretty-format@27.5.1:
|
|
||||||
dependencies:
|
|
||||||
ansi-regex: 5.0.1
|
|
||||||
ansi-styles: 5.2.0
|
|
||||||
react-is: 17.0.2
|
|
||||||
|
|
||||||
printable-characters@1.0.42: {}
|
printable-characters@1.0.42: {}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
|
@ -6556,11 +6478,6 @@ snapshots:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
|
|
||||||
react-error-boundary@4.0.13(react@18.2.0):
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.0
|
|
||||||
react: 18.2.0
|
|
||||||
|
|
||||||
react-fast-compare@2.0.4: {}
|
react-fast-compare@2.0.4: {}
|
||||||
|
|
||||||
react-fast-compare@3.2.2: {}
|
react-fast-compare@3.2.2: {}
|
||||||
|
@ -6579,8 +6496,6 @@ snapshots:
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-is@17.0.2: {}
|
|
||||||
|
|
||||||
react-multi-select-component@4.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
react-multi-select-component@4.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
@ -6728,11 +6643,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.14.3):
|
rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.17.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.24.2
|
'@babel/code-frame': 7.24.2
|
||||||
jest-worker: 26.6.2
|
jest-worker: 26.6.2
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
serialize-javascript: 4.0.0
|
serialize-javascript: 4.0.0
|
||||||
terser: 5.30.1
|
terser: 5.30.1
|
||||||
|
|
||||||
|
@ -7150,9 +7065,9 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite-plugin-svgr@4.2.0(@rollup/wasm-node@4.14.3)(typescript@5.4.5)(vite@5.2.9(@types/node@20.12.7)(terser@5.30.1)):
|
vite-plugin-svgr@4.2.0(@rollup/wasm-node@4.17.2)(typescript@5.4.5)(vite@5.2.9(@types/node@20.12.7)(terser@5.30.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.1.0(@rollup/wasm-node@4.14.3)
|
'@rollup/pluginutils': 5.1.0(@rollup/wasm-node@4.17.2)
|
||||||
'@svgr/core': 8.1.0(typescript@5.4.5)
|
'@svgr/core': 8.1.0(typescript@5.4.5)
|
||||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.4.5))
|
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.4.5))
|
||||||
vite: 5.2.9(@types/node@20.12.7)(terser@5.30.1)
|
vite: 5.2.9(@types/node@20.12.7)(terser@5.30.1)
|
||||||
|
@ -7165,7 +7080,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.20.2
|
esbuild: 0.20.2
|
||||||
postcss: 8.4.38
|
postcss: 8.4.38
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 20.12.7
|
'@types/node': 20.12.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
@ -7240,9 +7155,9 @@ snapshots:
|
||||||
'@babel/core': 7.24.3
|
'@babel/core': 7.24.3
|
||||||
'@babel/preset-env': 7.24.3(@babel/core@7.24.3)
|
'@babel/preset-env': 7.24.3(@babel/core@7.24.3)
|
||||||
'@babel/runtime': 7.24.1
|
'@babel/runtime': 7.24.1
|
||||||
'@rollup/plugin-babel': 5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.14.3)
|
'@rollup/plugin-babel': 5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.17.2)
|
||||||
'@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.14.3)
|
'@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.17.2)
|
||||||
'@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.14.3)
|
'@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.17.2)
|
||||||
'@surma/rollup-plugin-off-main-thread': 2.2.3
|
'@surma/rollup-plugin-off-main-thread': 2.2.3
|
||||||
ajv: 8.12.0
|
ajv: 8.12.0
|
||||||
common-tags: 1.8.2
|
common-tags: 1.8.2
|
||||||
|
@ -7251,8 +7166,8 @@ snapshots:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
pretty-bytes: 5.6.0
|
pretty-bytes: 5.6.0
|
||||||
rollup: '@rollup/wasm-node@4.14.3'
|
rollup: '@rollup/wasm-node@4.17.2'
|
||||||
rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.14.3)
|
rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.17.2)
|
||||||
source-map: 0.8.0-beta.0
|
source-map: 0.8.0-beta.0
|
||||||
stringify-object: 3.3.0
|
stringify-object: 3.3.0
|
||||||
strip-comments: 2.0.1
|
strip-comments: 2.0.1
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Portal } from "react-portal";
|
||||||
import { Router } from "@app/routes";
|
import { Router } from "@app/routes";
|
||||||
import { routerBasePath } from "@utils";
|
import { routerBasePath } from "@utils";
|
||||||
import { queryClient } from "@api/QueryClient";
|
import { queryClient } from "@api/QueryClient";
|
||||||
import { AuthContext, SettingsContext } from "@utils/Context";
|
import { SettingsContext } from "@utils/Context";
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface Register {
|
interface Register {
|
||||||
|
@ -40,9 +40,6 @@ export function App() {
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
basepath={routerBasePath()}
|
basepath={routerBasePath()}
|
||||||
router={Router}
|
router={Router}
|
||||||
context={{
|
|
||||||
auth: AuthContext,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,17 +5,65 @@
|
||||||
|
|
||||||
import { baseUrl, sseBaseUrl } from "@utils";
|
import { baseUrl, sseBaseUrl } from "@utils";
|
||||||
import { GithubRelease } from "@app/types/Update";
|
import { GithubRelease } from "@app/types/Update";
|
||||||
|
import { AuthContext } from "@utils/Context";
|
||||||
|
|
||||||
type RequestBody = BodyInit | object | Record<string, unknown> | null;
|
type RequestBody = BodyInit | object | Record<string, unknown> | null;
|
||||||
type Primitive = string | number | boolean | symbol | undefined;
|
type Primitive = string | number | boolean | symbol | undefined;
|
||||||
|
|
||||||
interface HttpConfig {
|
interface HttpConfig {
|
||||||
|
/**
|
||||||
|
* One of "GET", "POST", "PUT", "PATCH", "DELETE", etc.
|
||||||
|
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||||
|
*/
|
||||||
method?: string;
|
method?: string;
|
||||||
|
/**
|
||||||
|
* JSON body for this request. Once this is set to an object,
|
||||||
|
* then `Content-Type` for this request is set to `application/json`
|
||||||
|
* automatically.
|
||||||
|
*/
|
||||||
body?: RequestBody;
|
body?: RequestBody;
|
||||||
|
/**
|
||||||
|
* Helper to work with a query string/search param of a URL.
|
||||||
|
* E.g. ?a=1&b=2&c=3
|
||||||
|
*
|
||||||
|
* Using this interface will automatically convert
|
||||||
|
* the object values into RFC-3986-compliant strings.
|
||||||
|
*
|
||||||
|
* Keys will *NOT* be sanitized, and any whitespace and
|
||||||
|
* invalid characters will remain.
|
||||||
|
*
|
||||||
|
* The only supported value types are:
|
||||||
|
* numbers, booleans, strings and flat 1-D arrays.
|
||||||
|
*
|
||||||
|
* Objects as values are not supported.
|
||||||
|
*
|
||||||
|
* The supported values are serialized as follows:
|
||||||
|
* - undefined values are ignored
|
||||||
|
* - empty strings are ignored
|
||||||
|
* - empty strings inside arrays are ignored
|
||||||
|
* - empty arrays are ignored
|
||||||
|
* - arrays append each time with the key and for each child
|
||||||
|
* e.g. `{ arr: [1, 2, 3] }` will yield `?arr=1&arr=2&arr=3`
|
||||||
|
* - array items with an undefined value (or which serialize to an empty string) are ignored,
|
||||||
|
* e.g. `{ arr: [1, undefined, undefined] }` will yield `?arr=1`
|
||||||
|
* (NaN, +Inf, -Inf, etc. will remain since they are valid serializations)
|
||||||
|
*/
|
||||||
queryString?: Record<string, Primitive | Primitive[]>;
|
queryString?: Record<string, Primitive | Primitive[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://stackoverflow.com/a/62969380
|
/**
|
||||||
|
* Encodes a string into a RFC-3986-compliant string.
|
||||||
|
*
|
||||||
|
* By default, encodeURIComponent will not encode
|
||||||
|
* any of the following characters: !'()*
|
||||||
|
*
|
||||||
|
* So a simple regex replace is done which will replace
|
||||||
|
* these characters with their hex-value representation.
|
||||||
|
*
|
||||||
|
* @param str Input string (dictionary value).
|
||||||
|
* @returns A RFC-3986-compliant string variation of the input string.
|
||||||
|
* @note See https://stackoverflow.com/a/62969380
|
||||||
|
*/
|
||||||
function encodeRFC3986URIComponent(str: string): string {
|
function encodeRFC3986URIComponent(str: string): string {
|
||||||
return encodeURIComponent(str).replace(
|
return encodeURIComponent(str).replace(
|
||||||
/[!'()*]/g,
|
/[!'()*]/g,
|
||||||
|
@ -23,6 +71,29 @@ function encodeRFC3986URIComponent(str: string): string {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request on the network and returns a promise.
|
||||||
|
*
|
||||||
|
* This function serves as both a request builder and a response interceptor.
|
||||||
|
*
|
||||||
|
* @param endpoint The endpoint path relative to the backend instance.
|
||||||
|
* @param config A dictionary which specifies what information this network
|
||||||
|
* request must relay during transport. See @ref HttpClient.
|
||||||
|
* @returns A promise for the *sent* network request which must * be await'ed or .then()-chained before it can be used.
|
||||||
|
*
|
||||||
|
* If the status code returned by the server is in the [200, 300) range, then this is considered a success.
|
||||||
|
* - This function resolves with an empty dictionary object, i.e. {}, if the status code is 204 No data
|
||||||
|
* - The parsed JSON body is returned by this method if the server returns `Content-Type: application/json`.
|
||||||
|
* - In all other scenarios, the raw Response object from window.fetch() is returned,
|
||||||
|
* which must be handled manually by awaiting on one of its methods.
|
||||||
|
*
|
||||||
|
* The following is done if the status code that the server returns is NOT successful,
|
||||||
|
* that is, if it falls outside of the [200, 300] range:
|
||||||
|
* - A unique Error object is returned if the user is logged in and the status code is 403 Forbidden.
|
||||||
|
* This Error object *should* be consumed by the @tanstack/query code, which indirectly calls HttpClient.
|
||||||
|
* The current user is then prompted to log in again after being logged out.
|
||||||
|
* - The `ErrorPage` screen appears in all other scenarios.
|
||||||
|
*/
|
||||||
export async function HttpClient<T = unknown>(
|
export async function HttpClient<T = unknown>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
config: HttpConfig = {}
|
config: HttpConfig = {}
|
||||||
|
@ -81,22 +152,31 @@ export async function HttpClient<T = unknown>(
|
||||||
|
|
||||||
const response = await window.fetch(`${baseUrl()}${endpoint}`, init);
|
const response = await window.fetch(`${baseUrl()}${endpoint}`, init);
|
||||||
|
|
||||||
const isJson = response.headers.get("Content-Type")?.includes("application/json");
|
if (response.status >= 200 && response.status < 300) {
|
||||||
const json = isJson ? await response.json() : null;
|
// We received a successful response
|
||||||
|
if (response.status === 204) {
|
||||||
switch (response.status) {
|
|
||||||
case 204: {
|
|
||||||
// 204 contains no data, but indicates success
|
// 204 contains no data, but indicates success
|
||||||
return Promise.resolve<T>({} as T);
|
return Promise.resolve<T>({} as T);
|
||||||
}
|
}
|
||||||
case 401: {
|
|
||||||
return Promise.reject<T>(json as T);
|
// If Content-Type is application/json, then parse response as JSON
|
||||||
|
// otherwise, just resolve the Response object returned by window.fetch
|
||||||
|
// and the consumer can call await response.text() if needed.
|
||||||
|
const isJson = response.headers.get("Content-Type")?.includes("application/json");
|
||||||
|
if (isJson) {
|
||||||
|
return Promise.resolve<T>(await response.json() as T);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve<T>(response as T);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// This is not a successful response.
|
||||||
|
// It is most likely an error.
|
||||||
|
switch (response.status) {
|
||||||
case 403: {
|
case 403: {
|
||||||
return Promise.reject<T>(json as T);
|
if (AuthContext.get().isLoggedIn) {
|
||||||
|
return Promise.reject(new Error("Cookie expired or invalid."));
|
||||||
}
|
}
|
||||||
case 404: {
|
break;
|
||||||
return Promise.reject<T>(json as T);
|
|
||||||
}
|
}
|
||||||
case 500: {
|
case 500: {
|
||||||
const health = await window.fetch(`${baseUrl()}api/healthz/liveness`);
|
const health = await window.fetch(`${baseUrl()}api/healthz/liveness`);
|
||||||
|
@ -115,19 +195,13 @@ export async function HttpClient<T = unknown>(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve on success
|
const defaultError = new Error(
|
||||||
if (response.status >= 200 && response.status < 300) {
|
`HTTP request to '${endpoint}' failed with code ${response.status} (${response.statusText})`
|
||||||
if (isJson) {
|
);
|
||||||
return Promise.resolve<T>(json as T);
|
return Promise.reject(defaultError);
|
||||||
} else {
|
|
||||||
return Promise.resolve<T>(response as T);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise reject, this is most likely an error
|
|
||||||
return Promise.reject<T>(json as T);
|
|
||||||
}
|
|
||||||
|
|
||||||
const appClient = {
|
const appClient = {
|
||||||
Get: <T>(endpoint: string, config: HttpConfig = {}) => HttpClient<T>(endpoint, {
|
Get: <T>(endpoint: string, config: HttpConfig = {}) => HttpClient<T>(endpoint, {
|
||||||
...config,
|
...config,
|
||||||
|
|
|
@ -6,26 +6,31 @@
|
||||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { baseUrl } from "@utils";
|
import { AuthContext } from "@utils/Context";
|
||||||
|
import { redirect } from "@tanstack/react-router";
|
||||||
|
import { LoginRoute } from "@app/routes";
|
||||||
|
|
||||||
const MAX_RETRIES = 6;
|
const MAX_RETRIES = 6;
|
||||||
const HTTP_STATUS_TO_NOT_RETRY = [400, 401, 403, 404];
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
queryCache: new QueryCache({
|
queryCache: new QueryCache({
|
||||||
onError: (error ) => {
|
onError: (error, query) => {
|
||||||
console.error("query client error: ", error);
|
console.error(`Caught error for query '${query.queryKey}': `, error);
|
||||||
|
|
||||||
|
if (error.message === "Cookie expired or invalid.") {
|
||||||
|
AuthContext.reset();
|
||||||
|
redirect({
|
||||||
|
to: LoginRoute.to,
|
||||||
|
search: {
|
||||||
|
// Use the current location to power a redirect after login
|
||||||
|
// (Do not use `router.state.resolvedLocation` as it can
|
||||||
|
// potentially lag behind the actual current location)
|
||||||
|
redirect: location.href
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
toast.custom((t) => <Toast type="error" body={ error?.message } t={ t }/>);
|
toast.custom((t) => <Toast type="error" body={ error?.message } t={ t }/>);
|
||||||
|
|
||||||
// @ts-expect-error TS2339: Property status does not exist on type Error
|
|
||||||
if (error?.status === 401 || error?.status === 403) {
|
|
||||||
// @ts-expect-error TS2339: Property status does not exist on type Error
|
|
||||||
console.error("bad status, redirect to login", error?.status)
|
|
||||||
// Redirect to login page
|
|
||||||
window.location.href = baseUrl()+"login";
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -35,8 +40,12 @@ export const queryClient = new QueryClient({
|
||||||
// See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay
|
// See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay
|
||||||
// delay = Math.min(1000 * 2 ** attemptIndex, 30000)
|
// delay = Math.min(1000 * 2 ** attemptIndex, 30000)
|
||||||
// retry: false,
|
// retry: false,
|
||||||
throwOnError: true,
|
throwOnError: (error) => {
|
||||||
|
return error.message !== "Cookie expired or invalid.";
|
||||||
|
|
||||||
|
},
|
||||||
retry: (failureCount, error) => {
|
retry: (failureCount, error) => {
|
||||||
|
/*
|
||||||
console.debug("retry count:", failureCount)
|
console.debug("retry count:", failureCount)
|
||||||
console.error("retry err: ", error)
|
console.error("retry err: ", error)
|
||||||
|
|
||||||
|
@ -46,7 +55,12 @@ export const queryClient = new QueryClient({
|
||||||
console.log(`retry: Aborting retry due to ${error.status} status`);
|
console.log(`retry: Aborting retry due to ${error.status} status`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
if (error.message === "Cookie expired or invalid.") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Retrying query (N=${failureCount}): `, error);
|
||||||
return failureCount <= MAX_RETRIES;
|
return failureCount <= MAX_RETRIES;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -54,8 +68,9 @@ export const queryClient = new QueryClient({
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.log("mutation error: ", error)
|
console.log("mutation error: ", error)
|
||||||
|
|
||||||
|
// TODO: Maybe unneeded with our little HttpClient refactor.
|
||||||
if (error instanceof Response) {
|
if (error instanceof Response) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a format string to convert the error object to a proper string without much hassle.
|
// Use a format string to convert the error object to a proper string without much hassle.
|
||||||
|
|
|
@ -4,13 +4,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import StackTracey from "stacktracey";
|
import StackTracey from "stacktracey";
|
||||||
import type { FallbackProps } from "react-error-boundary";
|
|
||||||
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||||
import { ExternalLink } from "@components/ExternalLink";
|
import { ExternalLink } from "@components/ExternalLink";
|
||||||
|
|
||||||
export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
type ErrorPageProps = {
|
||||||
|
error: unknown;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorPage = ({ error, reset }: ErrorPageProps) => {
|
||||||
|
let pageTitle = "We caught an unrecoverable error!";
|
||||||
|
let errorLine: string, summary ="";
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
const stack = new StackTracey(error);
|
const stack = new StackTracey(error);
|
||||||
const summary = stack.clean().asTable({
|
summary = stack.clean().asTable({
|
||||||
maxColumnWidths: {
|
maxColumnWidths: {
|
||||||
callee: 48,
|
callee: 48,
|
||||||
file: 48,
|
file: 48,
|
||||||
|
@ -18,22 +26,21 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const parseTitle = () => {
|
if (error.cause === "OFFLINE") {
|
||||||
switch (error?.cause) {
|
pageTitle = "Connection to Autobrr failed! Check the application state and verify your connectivity.";
|
||||||
case "OFFLINE": {
|
|
||||||
return "Connection to Autobrr failed! Check the application state and verify your connectivity.";
|
|
||||||
}
|
}
|
||||||
default: {
|
|
||||||
return "We caught an unrecoverable error!";
|
errorLine = error.toString();
|
||||||
|
} else {
|
||||||
|
errorLine = String(error);
|
||||||
|
// Leave summary blank?
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col justify-center py-12 px-2 sm:px-6 lg:px-8">
|
<div className="min-h-screen flex flex-col justify-center py-12 px-2 sm:px-6 lg:px-8">
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-screen-md md:max-w-screen-lg lg:max-w-screen-xl">
|
<div className="sm:mx-auto sm:w-full sm:max-w-screen-md md:max-w-screen-lg lg:max-w-screen-xl">
|
||||||
<h1 className="text-3xl font-bold leading-6 text-gray-900 dark:text-gray-200 mt-4 mb-3">
|
<h1 className="text-3xl font-bold leading-6 text-gray-900 dark:text-gray-200 mt-4 mb-3">
|
||||||
{parseTitle()}
|
{pageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<h3 className="text-xl leading-6 text-gray-700 dark:text-gray-400 mb-4">
|
<h3 className="text-xl leading-6 text-gray-700 dark:text-gray-400 mb-4">
|
||||||
Please consider reporting this error to our
|
Please consider reporting this error to our
|
||||||
|
@ -67,7 +74,7 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 className="text-lg font-medium text-red-700 dark:text-red-800">{error.toString()}</h3>
|
<h3 className="text-lg font-medium text-red-700 dark:text-red-800">{errorLine}</h3>
|
||||||
</div>
|
</div>
|
||||||
{summary ? (
|
{summary ? (
|
||||||
<pre className="mt-2 mb-4 text-sm text-red-700 dark:text-red-800 overflow-x-auto">
|
<pre className="mt-2 mb-4 text-sm text-red-700 dark:text-red-800 overflow-x-auto">
|
||||||
|
@ -83,7 +90,7 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
className="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 mr-2 text-center inline-flex items-center dark:bg-red-800 dark:hover:bg-red-900"
|
className="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 mr-2 text-center inline-flex items-center dark:bg-red-800 dark:hover:bg-red-900"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
resetErrorBoundary();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowPathIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
<ArrowPathIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
||||||
|
|
|
@ -16,13 +16,11 @@ import { LeftNav } from "./LeftNav";
|
||||||
import { RightNav } from "./RightNav";
|
import { RightNav } from "./RightNav";
|
||||||
import { MobileNav } from "./MobileNav";
|
import { MobileNav } from "./MobileNav";
|
||||||
import { ExternalLink } from "@components/ExternalLink";
|
import { ExternalLink } from "@components/ExternalLink";
|
||||||
|
|
||||||
import { AuthIndexRoute } from "@app/routes";
|
|
||||||
import { ConfigQueryOptions, UpdatesQueryOptions } from "@api/queries";
|
import { ConfigQueryOptions, UpdatesQueryOptions } from "@api/queries";
|
||||||
|
import { AuthContext } from "@utils/Context";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { auth } = AuthIndexRoute.useRouteContext()
|
|
||||||
|
|
||||||
const { isError:isConfigError, error: configError, data: config } = useQuery(ConfigQueryOptions(true));
|
const { isError:isConfigError, error: configError, data: config } = useQuery(ConfigQueryOptions(true));
|
||||||
if (isConfigError) {
|
if (isConfigError) {
|
||||||
|
@ -40,9 +38,8 @@ export const Header = () => {
|
||||||
toast.custom((t) => (
|
toast.custom((t) => (
|
||||||
<Toast type="success" body="You have been logged out. Goodbye!" t={t} />
|
<Toast type="success" body="You have been logged out. Goodbye!" t={t} />
|
||||||
));
|
));
|
||||||
auth.logout()
|
AuthContext.reset();
|
||||||
|
router.history.push("/");
|
||||||
router.history.push("/")
|
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error("logout error", err)
|
console.error("logout error", err)
|
||||||
|
@ -60,7 +57,7 @@ export const Header = () => {
|
||||||
<div className="border-b border-gray-300 dark:border-gray-775">
|
<div className="border-b border-gray-300 dark:border-gray-775">
|
||||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||||
<LeftNav />
|
<LeftNav />
|
||||||
<RightNav logoutMutation={logoutMutation.mutate} auth={auth} />
|
<RightNav logoutMutation={logoutMutation.mutate} />
|
||||||
<div className="-mr-2 flex sm:hidden">
|
<div className="-mr-2 flex sm:hidden">
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
<Disclosure.Button className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
<Disclosure.Button className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||||
|
@ -92,7 +89,7 @@ export const Header = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MobileNav logoutMutation={logoutMutation.mutate} auth={auth} />
|
<MobileNav logoutMutation={logoutMutation.mutate} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { RightNavProps } from "./_shared";
|
||||||
|
|
||||||
import { Cog6ToothIcon, ArrowLeftOnRectangleIcon, MoonIcon, SunIcon } from "@heroicons/react/24/outline";
|
import { Cog6ToothIcon, ArrowLeftOnRectangleIcon, MoonIcon, SunIcon } from "@heroicons/react/24/outline";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { SettingsContext } from "@utils/Context";
|
import { AuthContext, SettingsContext } from "@utils/Context";
|
||||||
|
|
||||||
export const RightNav = (props: RightNavProps) => {
|
export const RightNav = (props: RightNavProps) => {
|
||||||
const [settings, setSettings] = SettingsContext.use();
|
const [settings, setSettings] = SettingsContext.use();
|
||||||
|
@ -56,7 +56,7 @@ export const RightNav = (props: RightNavProps) => {
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
Open user menu for{" "}
|
Open user menu for{" "}
|
||||||
</span>
|
</span>
|
||||||
{props.auth.username}
|
{AuthContext.get().username}
|
||||||
</span>
|
</span>
|
||||||
<UserIcon
|
<UserIcon
|
||||||
className="inline ml-1 h-5 w-5"
|
className="inline ml-1 h-5 w-5"
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AuthCtx } from "@utils/Context";
|
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -13,7 +11,6 @@ interface NavItem {
|
||||||
|
|
||||||
export interface RightNavProps {
|
export interface RightNavProps {
|
||||||
logoutMutation: () => void;
|
logoutMutation: () => void;
|
||||||
auth: AuthCtx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NAV_ROUTES: Array<NavItem> = [
|
export const NAV_ROUTES: Array<NavItem> = [
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
createRoute,
|
createRoute,
|
||||||
createRouter,
|
createRouter,
|
||||||
ErrorComponent,
|
Navigate,
|
||||||
notFound,
|
notFound,
|
||||||
Outlet,
|
Outlet,
|
||||||
redirect,
|
redirect,
|
||||||
|
@ -46,11 +46,13 @@ import DownloadClientSettings from "@screens/settings/DownloadClient";
|
||||||
import FeedSettings from "@screens/settings/Feed";
|
import FeedSettings from "@screens/settings/Feed";
|
||||||
import { Dashboard } from "@screens/Dashboard";
|
import { Dashboard } from "@screens/Dashboard";
|
||||||
import AccountSettings from "@screens/settings/Account";
|
import AccountSettings from "@screens/settings/Account";
|
||||||
import { AuthContext, AuthCtx, localStorageUserKey, SettingsContext } from "@utils/Context";
|
import { AuthContext, SettingsContext } from "@utils/Context";
|
||||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { queryClient } from "@api/QueryClient";
|
import { queryClient } from "@api/QueryClient";
|
||||||
|
|
||||||
|
import { ErrorPage } from "@components/alerts";
|
||||||
|
|
||||||
const DashboardRoute = createRoute({
|
const DashboardRoute = createRoute({
|
||||||
getParentRoute: () => AuthIndexRoute,
|
getParentRoute: () => AuthIndexRoute,
|
||||||
path: '/',
|
path: '/',
|
||||||
|
@ -133,16 +135,9 @@ export const FilterActionsRoute = createRoute({
|
||||||
component: Actions
|
component: Actions
|
||||||
});
|
});
|
||||||
|
|
||||||
const ReleasesRoute = createRoute({
|
export const ReleasesRoute = createRoute({
|
||||||
getParentRoute: () => AuthIndexRoute,
|
getParentRoute: () => AuthIndexRoute,
|
||||||
path: 'releases'
|
path: 'releases',
|
||||||
});
|
|
||||||
|
|
||||||
// type ReleasesSearch = z.infer<typeof releasesSearchSchema>
|
|
||||||
|
|
||||||
export const ReleasesIndexRoute = createRoute({
|
|
||||||
getParentRoute: () => ReleasesRoute,
|
|
||||||
path: '/',
|
|
||||||
component: Releases,
|
component: Releases,
|
||||||
validateSearch: (search) => z.object({
|
validateSearch: (search) => z.object({
|
||||||
offset: z.number().optional(),
|
offset: z.number().optional(),
|
||||||
|
@ -279,22 +274,7 @@ export const AuthRoute = createRoute({
|
||||||
// This will also happen during prefetching (e.g. hovering over links, etc.)
|
// This will also happen during prefetching (e.g. hovering over links, etc.)
|
||||||
beforeLoad: ({ context, location }) => {
|
beforeLoad: ({ context, location }) => {
|
||||||
// If the user is not logged in, check for item in localStorage
|
// If the user is not logged in, check for item in localStorage
|
||||||
if (!context.auth.isLoggedIn) {
|
if (!AuthContext.get().isLoggedIn) {
|
||||||
const storage = localStorage.getItem(localStorageUserKey);
|
|
||||||
if (storage) {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(storage);
|
|
||||||
if (json === null) {
|
|
||||||
console.warn(`JSON localStorage value for '${localStorageUserKey}' context state is null`);
|
|
||||||
} else {
|
|
||||||
context.auth.isLoggedIn = json.isLoggedIn
|
|
||||||
context.auth.username = json.username
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`auth Failed to merge ${localStorageUserKey} context state: ${e}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the user is logged out, redirect them to the login page
|
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: LoginRoute.to,
|
to: LoginRoute.to,
|
||||||
search: {
|
search: {
|
||||||
|
@ -303,18 +283,25 @@ export const AuthRoute = createRoute({
|
||||||
// potentially lag behind the actual current location)
|
// potentially lag behind the actual current location)
|
||||||
redirect: location.href,
|
redirect: location.href,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, return the user in context
|
// Otherwise, return the user in context
|
||||||
return {
|
return context;
|
||||||
username: AuthContext.username,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function AuthenticatedLayout() {
|
function AuthenticatedLayout() {
|
||||||
|
const isLoggedIn = AuthContext.useSelector((s) => s.isLoggedIn);
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
const redirect = (
|
||||||
|
location.pathname.length > 1
|
||||||
|
? { redirect: location.pathname }
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
return <Navigate to="/login" search={redirect} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<Header/>
|
<Header/>
|
||||||
|
@ -345,7 +332,6 @@ export const RootComponent = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RootRoute = createRootRouteWithContext<{
|
export const RootRoute = createRootRouteWithContext<{
|
||||||
auth: AuthCtx,
|
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
}>()({
|
}>()({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
|
@ -354,7 +340,7 @@ export const RootRoute = createRootRouteWithContext<{
|
||||||
|
|
||||||
const filterRouteTree = FiltersRoute.addChildren([FilterIndexRoute, FilterGetByIdRoute.addChildren([FilterGeneralRoute, FilterMoviesTvRoute, FilterMusicRoute, FilterAdvancedRoute, FilterExternalRoute, FilterActionsRoute])])
|
const filterRouteTree = FiltersRoute.addChildren([FilterIndexRoute, FilterGetByIdRoute.addChildren([FilterGeneralRoute, FilterMoviesTvRoute, FilterMusicRoute, FilterAdvancedRoute, FilterExternalRoute, FilterActionsRoute])])
|
||||||
const settingsRouteTree = SettingsRoute.addChildren([SettingsIndexRoute, SettingsLogRoute, SettingsIndexersRoute, SettingsIrcRoute, SettingsFeedsRoute, SettingsClientsRoute, SettingsNotificationsRoute, SettingsApiRoute, SettingsReleasesRoute, SettingsAccountRoute])
|
const settingsRouteTree = SettingsRoute.addChildren([SettingsIndexRoute, SettingsLogRoute, SettingsIndexersRoute, SettingsIrcRoute, SettingsFeedsRoute, SettingsClientsRoute, SettingsNotificationsRoute, SettingsApiRoute, SettingsReleasesRoute, SettingsAccountRoute])
|
||||||
const authenticatedTree = AuthRoute.addChildren([AuthIndexRoute.addChildren([DashboardRoute, filterRouteTree, ReleasesRoute.addChildren([ReleasesIndexRoute]), settingsRouteTree, LogsRoute])])
|
const authenticatedTree = AuthRoute.addChildren([AuthIndexRoute.addChildren([DashboardRoute, filterRouteTree, ReleasesRoute, settingsRouteTree, LogsRoute])])
|
||||||
const routeTree = RootRoute.addChildren([
|
const routeTree = RootRoute.addChildren([
|
||||||
authenticatedTree,
|
authenticatedTree,
|
||||||
LoginRoute,
|
LoginRoute,
|
||||||
|
@ -368,9 +354,10 @@ export const Router = createRouter({
|
||||||
<RingResizeSpinner className="text-blue-500 size-24"/>
|
<RingResizeSpinner className="text-blue-500 size-24"/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
defaultErrorComponent: ({error}) => <ErrorComponent error={error}/>,
|
defaultErrorComponent: (ctx) => (
|
||||||
|
<ErrorPage error={ctx.error} reset={ctx.reset} />
|
||||||
|
),
|
||||||
context: {
|
context: {
|
||||||
auth: undefined!, // We'll inject this when we render
|
|
||||||
queryClient
|
queryClient
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,16 +26,16 @@ interface NavTabType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const subNavigation: NavTabType[] = [
|
const subNavigation: NavTabType[] = [
|
||||||
{ name: "Application", href: ".", icon: CogIcon, exact: true },
|
{ name: "Application", href: "/settings", icon: CogIcon, exact: true },
|
||||||
{ name: "Logs", href: "logs", icon: Square3Stack3DIcon },
|
{ name: "Logs", href: "/settings/logs", icon: Square3Stack3DIcon },
|
||||||
{ name: "Indexers", href: "indexers", icon: KeyIcon },
|
{ name: "Indexers", href: "/settings/indexers", icon: KeyIcon },
|
||||||
{ name: "IRC", href: "irc", icon: ChatBubbleLeftRightIcon },
|
{ name: "IRC", href: "/settings/irc", icon: ChatBubbleLeftRightIcon },
|
||||||
{ name: "Feeds", href: "feeds", icon: RssIcon },
|
{ name: "Feeds", href: "/settings/feeds", icon: RssIcon },
|
||||||
{ name: "Clients", href: "clients", icon: FolderArrowDownIcon },
|
{ name: "Clients", href: "/settings/clients", icon: FolderArrowDownIcon },
|
||||||
{ name: "Notifications", href: "notifications", icon: BellIcon },
|
{ name: "Notifications", href: "/settings/notifications", icon: BellIcon },
|
||||||
{ name: "API keys", href: "api", icon: KeyIcon },
|
{ name: "API keys", href: "/settings/api", icon: KeyIcon },
|
||||||
{ name: "Releases", href: "releases", icon: RectangleStackIcon },
|
{ name: "Releases", href: "/settings/releases", icon: RectangleStackIcon },
|
||||||
{ name: "Account", href: "account", icon: UserCircleIcon }
|
{ name: "Account", href: "/settings/account", icon: UserCircleIcon }
|
||||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQueryErrorResetBoundary } from "@tanstack/react-query";
|
||||||
import { useRouter, useSearch } from "@tanstack/react-router";
|
import { useRouter, useSearch } from "@tanstack/react-router";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ import { PasswordInput, TextInput } from "@components/inputs/text";
|
||||||
import { LoginRoute } from "@app/routes";
|
import { LoginRoute } from "@app/routes";
|
||||||
|
|
||||||
import Logo from "@app/logo.svg?react";
|
import Logo from "@app/logo.svg?react";
|
||||||
|
import { AuthContext } from "@utils/Context";
|
||||||
|
// import { WarningAlert } from "@components/alerts";
|
||||||
|
|
||||||
type LoginFormFields = {
|
type LoginFormFields = {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -25,8 +27,11 @@ type LoginFormFields = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
|
const [auth, setAuth] = AuthContext.use();
|
||||||
|
|
||||||
|
const queryErrorResetBoundary = useQueryErrorResetBoundary()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { auth } = LoginRoute.useRouteContext()
|
|
||||||
const search = useSearch({ from: LoginRoute.id })
|
const search = useSearch({ from: LoginRoute.id })
|
||||||
|
|
||||||
const { handleSubmit, register, formState } = useForm<LoginFormFields>({
|
const { handleSubmit, register, formState } = useForm<LoginFormFields>({
|
||||||
|
@ -35,14 +40,19 @@ export const Login = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
queryErrorResetBoundary.reset()
|
||||||
// remove user session when visiting login page
|
// remove user session when visiting login page
|
||||||
auth.logout()
|
AuthContext.reset();
|
||||||
}, []);
|
}, [queryErrorResetBoundary]);
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationFn: (data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
|
mutationFn: (data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
|
||||||
onSuccess: (_, variables: LoginFormFields) => {
|
onSuccess: (_, variables: LoginFormFields) => {
|
||||||
auth.login(variables.username)
|
queryErrorResetBoundary.reset()
|
||||||
|
setAuth({
|
||||||
|
isLoggedIn: true,
|
||||||
|
username: variables.username
|
||||||
|
});
|
||||||
router.invalidate()
|
router.invalidate()
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
@ -60,7 +70,7 @@ export const Login = () => {
|
||||||
} else if (auth.isLoggedIn) {
|
} else if (auth.isLoggedIn) {
|
||||||
router.history.push("/")
|
router.history.push("/")
|
||||||
}
|
}
|
||||||
}, [auth.isLoggedIn, search.redirect])
|
}, [auth.isLoggedIn, search.redirect]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col justify-center px-3">
|
<div className="min-h-screen flex flex-col justify-center px-3">
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const Onboarding = () => {
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
|
mutationFn: (data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
|
||||||
onSuccess: () => navigate({ to: "/" })
|
onSuccess: () => navigate({ to: "/login" })
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -33,12 +33,12 @@ interface tabType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs: tabType[] = [
|
const tabs: tabType[] = [
|
||||||
{ name: "General", href: ".", exact: true },
|
{ name: "General", href: "/filters/$filterId", exact: true },
|
||||||
{ name: "Movies and TV", href: "movies-tv" },
|
{ name: "Movies and TV", href: "/filters/$filterId/movies-tv" },
|
||||||
{ name: "Music", href: "music" },
|
{ name: "Music", href: "/filters/$filterId/music" },
|
||||||
{ name: "Advanced", href: "advanced" },
|
{ name: "Advanced", href: "/filters/$filterId/advanced" },
|
||||||
{ name: "External", href: "external" },
|
{ name: "External", href: "/filters/$filterId/external" },
|
||||||
{ name: "Actions", href: "actions" }
|
{ name: "Actions", href: "/filters/$filterId/actions" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface NavLinkProps {
|
export interface NavLinkProps {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
EyeSlashIcon
|
EyeSlashIcon
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
import { ReleasesIndexRoute } from "@app/routes";
|
import { ReleasesRoute } from "@app/routes";
|
||||||
import { ReleasesListQueryOptions } from "@api/queries";
|
import { ReleasesListQueryOptions } from "@api/queries";
|
||||||
import { RandomLinuxIsos } from "@utils";
|
import { RandomLinuxIsos } from "@utils";
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ const EmptyReleaseList = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ReleaseTable = () => {
|
export const ReleaseTable = () => {
|
||||||
const search = ReleasesIndexRoute.useSearch()
|
const search = ReleasesRoute.useSearch()
|
||||||
|
|
||||||
const columns = React.useMemo(() => [
|
const columns = React.useMemo(() => [
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,12 +8,11 @@ import { Form, Formik } from "formik";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { UserIcon } from "@heroicons/react/24/solid";
|
import { UserIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
import { SettingsAccountRoute } from "@app/routes";
|
|
||||||
import { AuthContext } from "@utils/Context";
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { Section } from "./_components";
|
import { Section } from "./_components";
|
||||||
import { PasswordField, TextField } from "@components/inputs";
|
import { PasswordField, TextField } from "@components/inputs";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
import { AuthContext } from "@utils/Context";
|
||||||
|
|
||||||
const AccountSettings = () => (
|
const AccountSettings = () => (
|
||||||
<Section
|
<Section
|
||||||
|
@ -35,7 +34,7 @@ interface InputValues {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Credentials() {
|
function Credentials() {
|
||||||
const ctx = SettingsAccountRoute.useRouteContext()
|
const username = AuthContext.useSelector((s) => s.username);
|
||||||
|
|
||||||
const validate = (values: InputValues) => {
|
const validate = (values: InputValues) => {
|
||||||
const errors: Record<string, string> = {};
|
const errors: Record<string, string> = {};
|
||||||
|
@ -52,7 +51,7 @@ function Credentials() {
|
||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
mutationFn: APIClient.auth.logout,
|
mutationFn: APIClient.auth.logout,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
AuthContext.logout();
|
AuthContext.reset();
|
||||||
|
|
||||||
toast.custom((t) => (
|
toast.custom((t) => (
|
||||||
<Toast type="success" body="User updated successfully. Please sign in again!" t={t} />
|
<Toast type="success" body="User updated successfully. Please sign in again!" t={t} />
|
||||||
|
@ -78,7 +77,7 @@ function Credentials() {
|
||||||
<div className="px-2 pb-6 bg-white dark:bg-gray-800">
|
<div className="px-2 pb-6 bg-white dark:bg-gray-800">
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
username: ctx.auth.username!,
|
username: username,
|
||||||
newUsername: "",
|
newUsername: "",
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
|
|
|
@ -21,16 +21,16 @@ export type FilterListState = {
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// interface AuthInfo {
|
interface AuthInfo {
|
||||||
// username: string;
|
username: string;
|
||||||
// isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
// const AuthContextDefaults: AuthInfo = {
|
const AuthContextDefaults: AuthInfo = {
|
||||||
// username: "",
|
username: "",
|
||||||
// isLoggedIn: false
|
isLoggedIn: false
|
||||||
// };
|
};
|
||||||
|
|
||||||
const SettingsContextDefaults: SettingsType = {
|
const SettingsContextDefaults: SettingsType = {
|
||||||
debug: false,
|
debug: false,
|
||||||
|
@ -72,11 +72,12 @@ function ContextMerger<T extends {}>(
|
||||||
ctxState.set(values);
|
ctxState.set(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AuthKey = "autobrr_user_auth";
|
||||||
const SettingsKey = "autobrr_settings";
|
const SettingsKey = "autobrr_settings";
|
||||||
const FilterListKey = "autobrr_filter_list";
|
const FilterListKey = "autobrr_filter_list";
|
||||||
|
|
||||||
export const InitializeGlobalContext = () => {
|
export const InitializeGlobalContext = () => {
|
||||||
// ContextMerger<AuthInfo>(localStorageUserKey, AuthContextDefaults, AuthContextt);
|
ContextMerger<AuthInfo>(AuthKey, AuthContextDefaults, AuthContext);
|
||||||
ContextMerger<SettingsType>(
|
ContextMerger<SettingsType>(
|
||||||
SettingsKey,
|
SettingsKey,
|
||||||
SettingsContextDefaults,
|
SettingsContextDefaults,
|
||||||
|
@ -101,9 +102,12 @@ function DefaultSetter<T>(name: string, newState: T, prevState: T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const AuthContextt = newRidgeState<AuthInfo>(AuthContextDefaults, {
|
export const AuthContext = newRidgeState<AuthInfo>(
|
||||||
// onSet: (newState, prevState) => DefaultSetter(localStorageUserKey, newState, prevState)
|
AuthContextDefaults,
|
||||||
// });
|
{
|
||||||
|
onSet: (newState, prevState) => DefaultSetter(AuthKey, newState, prevState)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const SettingsContext = newRidgeState<SettingsType>(
|
export const SettingsContext = newRidgeState<SettingsType>(
|
||||||
SettingsContextDefaults,
|
SettingsContextDefaults,
|
||||||
|
@ -121,29 +125,3 @@ export const FilterListContext = newRidgeState<FilterListState>(
|
||||||
onSet: (newState, prevState) => DefaultSetter(FilterListKey, newState, prevState)
|
onSet: (newState, prevState) => DefaultSetter(FilterListKey, newState, prevState)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type AuthCtx = {
|
|
||||||
isLoggedIn: boolean
|
|
||||||
username?: string
|
|
||||||
login: (username: string) => void
|
|
||||||
logout: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const localStorageUserKey = "autobrr_user_auth"
|
|
||||||
|
|
||||||
export const AuthContext: AuthCtx = {
|
|
||||||
isLoggedIn: false,
|
|
||||||
username: undefined,
|
|
||||||
login: (username: string) => {
|
|
||||||
AuthContext.isLoggedIn = true
|
|
||||||
AuthContext.username = username
|
|
||||||
|
|
||||||
localStorage.setItem(localStorageUserKey, JSON.stringify(AuthContext));
|
|
||||||
},
|
|
||||||
logout: () => {
|
|
||||||
AuthContext.isLoggedIn = false
|
|
||||||
AuthContext.username = undefined
|
|
||||||
|
|
||||||
localStorage.removeItem(localStorageUserKey);
|
|
||||||
},
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue