added hide and unhide password button
This commit is contained in:
69
package-lock.json
generated
69
package-lock.json
generated
@@ -564,6 +564,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -580,6 +581,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -596,6 +598,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -612,6 +615,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -628,6 +632,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -644,6 +649,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -660,6 +666,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -676,6 +683,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -692,6 +700,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -708,6 +717,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -724,6 +734,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -740,6 +751,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -756,6 +768,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -772,6 +785,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -788,6 +802,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -804,6 +819,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -820,6 +836,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -836,6 +853,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -852,6 +870,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -868,6 +887,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -884,6 +904,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -900,6 +921,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -916,6 +938,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -932,6 +955,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -948,6 +972,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -964,6 +989,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2832,6 +2858,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2845,6 +2872,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2858,6 +2886,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2871,6 +2900,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2884,6 +2914,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2897,6 +2928,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2910,6 +2942,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2923,6 +2956,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2936,6 +2970,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2949,6 +2984,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2962,6 +2998,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2975,6 +3012,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2988,6 +3026,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3001,6 +3040,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3014,6 +3054,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3027,6 +3068,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3040,6 +3082,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3053,6 +3096,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3066,6 +3110,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3079,6 +3124,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3671,7 +3717,7 @@
|
|||||||
"version": "7.6.13",
|
"version": "7.6.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -3710,6 +3756,7 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
@@ -3815,7 +3862,7 @@
|
|||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -3825,7 +3872,7 @@
|
|||||||
"version": "19.1.6",
|
"version": "19.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||||
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
@@ -5123,7 +5170,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
@@ -5500,6 +5547,7 @@
|
|||||||
"version": "0.25.6",
|
"version": "0.25.6",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||||
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -6086,6 +6134,7 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -7445,6 +7494,7 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
@@ -7473,6 +7523,7 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7508,6 +7559,7 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -7903,6 +7955,7 @@
|
|||||||
"version": "4.45.0",
|
"version": "4.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz",
|
||||||
"integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==",
|
"integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
@@ -8422,6 +8475,7 @@
|
|||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -8438,6 +8492,7 @@
|
|||||||
"version": "6.4.6",
|
"version": "6.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"picomatch": "^3 || ^4"
|
"picomatch": "^3 || ^4"
|
||||||
@@ -8452,6 +8507,7 @@
|
|||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -8616,7 +8672,7 @@
|
|||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -8793,6 +8849,7 @@
|
|||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
||||||
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
|
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@@ -8867,6 +8924,7 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -8884,6 +8942,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|||||||
@@ -8,443 +8,474 @@ import {Input} from "@/components/ui/input.tsx";
|
|||||||
import {Label} from "@/components/ui/label.tsx";
|
import {Label} from "@/components/ui/label.tsx";
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table.tsx";
|
} from "@/components/ui/table.tsx";
|
||||||
import {Shield, Trash2, Users} from "lucide-react";
|
// 🎯 Import the Eye and EyeOff icons from lucide-react
|
||||||
|
import {Shield, Trash2, Users, Eye, EyeOff} from "lucide-react";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {
|
import {
|
||||||
getOIDCConfig,
|
getOIDCConfig,
|
||||||
getRegistrationAllowed,
|
getRegistrationAllowed,
|
||||||
getUserList,
|
getUserList,
|
||||||
updateRegistrationAllowed,
|
updateRegistrationAllowed,
|
||||||
updateOIDCConfig,
|
updateOIDCConfig,
|
||||||
makeUserAdmin,
|
makeUserAdmin,
|
||||||
removeAdminStatus,
|
removeAdminStatus,
|
||||||
deleteUser
|
deleteUser
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
return document.cookie.split('; ').reduce((r, v) => {
|
return document.cookie.split('; ').reduce((r, v) => {
|
||||||
const parts = v.split('=');
|
const parts = v = v.split('=');
|
||||||
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
return parts[0] === name ? decodeURIComponent(parts[1]) : r;
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdminSettingsProps {
|
interface AdminSettingsProps {
|
||||||
isTopbarOpen?: boolean;
|
isTopbarOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.ReactElement {
|
export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.ReactElement {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {state: sidebarState} = useSidebar();
|
const {state: sidebarState} = useSidebar();
|
||||||
|
|
||||||
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
||||||
const [regLoading, setRegLoading] = React.useState(false);
|
const [regLoading, setRegLoading] = React.useState(false);
|
||||||
|
|
||||||
const [oidcConfig, setOidcConfig] = React.useState({
|
// 🎯 New state to manage password visibility
|
||||||
client_id: '',
|
const [showClientSecret, setShowClientSecret] = React.useState(false);
|
||||||
client_secret: '',
|
|
||||||
issuer_url: '',
|
|
||||||
authorization_url: '',
|
|
||||||
token_url: '',
|
|
||||||
identifier_path: 'sub',
|
|
||||||
name_path: 'name',
|
|
||||||
scopes: 'openid email profile',
|
|
||||||
userinfo_url: ''
|
|
||||||
});
|
|
||||||
const [oidcLoading, setOidcLoading] = React.useState(false);
|
|
||||||
const [oidcError, setOidcError] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const [users, setUsers] = React.useState<Array<{
|
const [oidcConfig, setOidcConfig] = React.useState({
|
||||||
id: string;
|
client_id: '',
|
||||||
username: string;
|
client_secret: '',
|
||||||
is_admin: boolean;
|
issuer_url: '',
|
||||||
is_oidc: boolean
|
authorization_url: '',
|
||||||
}>>([]);
|
token_url: '',
|
||||||
const [usersLoading, setUsersLoading] = React.useState(false);
|
identifier_path: 'sub',
|
||||||
const [newAdminUsername, setNewAdminUsername] = React.useState("");
|
name_path: 'name',
|
||||||
const [makeAdminLoading, setMakeAdminLoading] = React.useState(false);
|
scopes: 'openid email profile',
|
||||||
const [makeAdminError, setMakeAdminError] = React.useState<string | null>(null);
|
userinfo_url: ''
|
||||||
|
});
|
||||||
|
const [oidcLoading, setOidcLoading] = React.useState(false);
|
||||||
|
const [oidcError, setOidcError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
const [users, setUsers] = React.useState<Array<{
|
||||||
const jwt = getCookie("jwt");
|
id: string;
|
||||||
if (!jwt) return;
|
username: string;
|
||||||
getOIDCConfig()
|
is_admin: boolean;
|
||||||
.then(res => {
|
is_oidc: boolean
|
||||||
if (res) setOidcConfig(res);
|
}>>([]);
|
||||||
})
|
const [usersLoading, setUsersLoading] = React.useState(false);
|
||||||
.catch(() => {
|
const [newAdminUsername, setNewAdminUsername] = React.useState("");
|
||||||
});
|
const [makeAdminLoading, setMakeAdminLoading] = React.useState(false);
|
||||||
fetchUsers();
|
const [makeAdminError, setMakeAdminError] = React.useState<string | null>(null);
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
getRegistrationAllowed()
|
const jwt = getCookie("jwt");
|
||||||
.then(res => {
|
if (!jwt) return;
|
||||||
if (typeof res?.allowed === 'boolean') {
|
getOIDCConfig()
|
||||||
setAllowRegistration(res.allowed);
|
.then(res => {
|
||||||
}
|
if (res) setOidcConfig(res);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
});
|
});
|
||||||
}, []);
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
React.useEffect(() => {
|
||||||
const jwt = getCookie("jwt");
|
getRegistrationAllowed()
|
||||||
if (!jwt) return;
|
.then(res => {
|
||||||
setUsersLoading(true);
|
if (typeof res?.allowed === 'boolean') {
|
||||||
try {
|
setAllowRegistration(res.allowed);
|
||||||
const response = await getUserList();
|
}
|
||||||
setUsers(response.users);
|
})
|
||||||
} finally {
|
.catch(() => {
|
||||||
setUsersLoading(false);
|
});
|
||||||
}
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleRegistration = async (checked: boolean) => {
|
const fetchUsers = async () => {
|
||||||
setRegLoading(true);
|
const jwt = getCookie("jwt");
|
||||||
const jwt = getCookie("jwt");
|
if (!jwt) return;
|
||||||
try {
|
setUsersLoading(true);
|
||||||
await updateRegistrationAllowed(checked);
|
try {
|
||||||
setAllowRegistration(checked);
|
const response = await getUserList();
|
||||||
} finally {
|
setUsers(response.users);
|
||||||
setRegLoading(false);
|
} finally {
|
||||||
}
|
setUsersLoading(false);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOIDCConfigSubmit = async (e: React.FormEvent) => {
|
const handleToggleRegistration = async (checked: boolean) => {
|
||||||
e.preventDefault();
|
setRegLoading(true);
|
||||||
setOidcLoading(true);
|
const jwt = getCookie("jwt");
|
||||||
setOidcError(null);
|
try {
|
||||||
|
await updateRegistrationAllowed(checked);
|
||||||
|
setAllowRegistration(checked);
|
||||||
|
} finally {
|
||||||
|
setRegLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url'];
|
const handleOIDCConfigSubmit = async (e: React.FormEvent) => {
|
||||||
const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]);
|
e.preventDefault();
|
||||||
if (missing.length > 0) {
|
setOidcLoading(true);
|
||||||
setOidcError(t('admin.missingRequiredFields', { fields: missing.join(', ') }));
|
setOidcError(null);
|
||||||
setOidcLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jwt = getCookie("jwt");
|
const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url'];
|
||||||
try {
|
const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]);
|
||||||
await updateOIDCConfig(oidcConfig);
|
if (missing.length > 0) {
|
||||||
toast.success(t('admin.oidcConfigurationUpdated'));
|
setOidcError(t('admin.missingRequiredFields', { fields: missing.join(', ') }));
|
||||||
} catch (err: any) {
|
setOidcLoading(false);
|
||||||
setOidcError(err?.response?.data?.error || t('admin.failedToUpdateOidcConfig'));
|
return;
|
||||||
} finally {
|
}
|
||||||
setOidcLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOIDCConfigChange = (field: string, value: string) => {
|
const jwt = getCookie("jwt");
|
||||||
setOidcConfig(prev => ({...prev, [field]: value}));
|
try {
|
||||||
};
|
await updateOIDCConfig(oidcConfig);
|
||||||
|
toast.success(t('admin.oidcConfigurationUpdated'));
|
||||||
|
} catch (err: any) {
|
||||||
|
setOidcError(err?.response?.data?.error || t('admin.failedToUpdateOidcConfig'));
|
||||||
|
} finally {
|
||||||
|
setOidcLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleMakeUserAdmin = async (e: React.FormEvent) => {
|
const handleOIDCConfigChange = (field: string, value: string) => {
|
||||||
e.preventDefault();
|
setOidcConfig(prev => ({...prev, [field]: value}));
|
||||||
if (!newAdminUsername.trim()) return;
|
};
|
||||||
setMakeAdminLoading(true);
|
|
||||||
setMakeAdminError(null);
|
|
||||||
const jwt = getCookie("jwt");
|
|
||||||
try {
|
|
||||||
await makeUserAdmin(newAdminUsername.trim());
|
|
||||||
toast.success(t('admin.userIsNowAdmin', { username: newAdminUsername }));
|
|
||||||
setNewAdminUsername("");
|
|
||||||
fetchUsers();
|
|
||||||
} catch (err: any) {
|
|
||||||
setMakeAdminError(err?.response?.data?.error || t('admin.failedToMakeUserAdmin'));
|
|
||||||
} finally {
|
|
||||||
setMakeAdminLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveAdminStatus = async (username: string) => {
|
const handleMakeUserAdmin = async (e: React.FormEvent) => {
|
||||||
if (!confirm(t('admin.removeAdminStatus', { username }))) return;
|
e.preventDefault();
|
||||||
const jwt = getCookie("jwt");
|
if (!newAdminUsername.trim()) return;
|
||||||
try {
|
setMakeAdminLoading(true);
|
||||||
await removeAdminStatus(username);
|
setMakeAdminError(null);
|
||||||
toast.success(t('admin.adminStatusRemoved', { username }));
|
const jwt = getCookie("jwt");
|
||||||
fetchUsers();
|
try {
|
||||||
} catch (err: any) {
|
await makeUserAdmin(newAdminUsername.trim());
|
||||||
console.error('Failed to remove admin status:', err);
|
toast.success(t('admin.userIsNowAdmin', { username: newAdminUsername }));
|
||||||
toast.error(t('admin.failedToRemoveAdminStatus'));
|
setNewAdminUsername("");
|
||||||
}
|
fetchUsers();
|
||||||
};
|
} catch (err: any) {
|
||||||
|
setMakeAdminError(err?.response?.data?.error || t('admin.failedToMakeUserAdmin'));
|
||||||
|
} finally {
|
||||||
|
setMakeAdminLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteUser = async (username: string) => {
|
const handleRemoveAdminStatus = async (username: string) => {
|
||||||
if (!confirm(t('admin.deleteUser', { username }))) return;
|
if (!confirm(t('admin.removeAdminStatus', { username }))) return;
|
||||||
const jwt = getCookie("jwt");
|
const jwt = getCookie("jwt");
|
||||||
try {
|
try {
|
||||||
await deleteUser(username);
|
await removeAdminStatus(username);
|
||||||
toast.success(t('admin.userDeletedSuccessfully', { username }));
|
toast.success(t('admin.adminStatusRemoved', { username }));
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to delete user:', err);
|
console.error('Failed to remove admin status:', err);
|
||||||
toast.error(t('admin.failedToDeleteUser'));
|
toast.error(t('admin.failedToRemoveAdminStatus'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
const handleDeleteUser = async (username: string) => {
|
||||||
const leftMarginPx = sidebarState === 'collapsed' ? 26 : 8;
|
if (!confirm(t('admin.deleteUser', { username }))) return;
|
||||||
const bottomMarginPx = 8;
|
const jwt = getCookie("jwt");
|
||||||
const wrapperStyle: React.CSSProperties = {
|
try {
|
||||||
marginLeft: leftMarginPx,
|
await deleteUser(username);
|
||||||
marginRight: 17,
|
toast.success(t('admin.userDeletedSuccessfully', { username }));
|
||||||
marginTop: topMarginPx,
|
fetchUsers();
|
||||||
marginBottom: bottomMarginPx,
|
} catch (err: any) {
|
||||||
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`
|
console.error('Failed to delete user:', err);
|
||||||
};
|
toast.error(t('admin.failedToDeleteUser'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||||
<div style={wrapperStyle}
|
const leftMarginPx = sidebarState === 'collapsed' ? 26 : 8;
|
||||||
className="bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden">
|
const bottomMarginPx = 8;
|
||||||
<div className="h-full w-full flex flex-col">
|
const wrapperStyle: React.CSSProperties = {
|
||||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
marginLeft: leftMarginPx,
|
||||||
<h1 className="font-bold text-lg">{t('admin.title')}</h1>
|
marginRight: 17,
|
||||||
</div>
|
marginTop: topMarginPx,
|
||||||
<Separator className="p-0.25 w-full"/>
|
marginBottom: bottomMarginPx,
|
||||||
|
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`
|
||||||
|
};
|
||||||
|
|
||||||
<div className="px-6 py-4 overflow-auto">
|
return (
|
||||||
<Tabs defaultValue="registration" className="w-full">
|
<div style={wrapperStyle}
|
||||||
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
className="bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden">
|
||||||
<TabsTrigger value="registration" className="flex items-center gap-2">
|
<div className="h-full w-full flex flex-col">
|
||||||
<Users className="h-4 w-4"/>
|
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||||
{t('admin.general')}
|
<h1 className="font-bold text-lg">{t('admin.title')}</h1>
|
||||||
</TabsTrigger>
|
</div>
|
||||||
<TabsTrigger value="oidc" className="flex items-center gap-2">
|
<Separator className="p-0.25 w-full"/>
|
||||||
<Shield className="h-4 w-4"/>
|
|
||||||
OIDC
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="users" className="flex items-center gap-2">
|
|
||||||
<Users className="h-4 w-4"/>
|
|
||||||
{t('admin.users')}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="admins" className="flex items-center gap-2">
|
|
||||||
<Shield className="h-4 w-4"/>
|
|
||||||
{t('admin.adminManagement')}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="registration" className="space-y-6">
|
<div className="px-6 py-4 overflow-auto">
|
||||||
<div className="space-y-4">
|
<Tabs defaultValue="registration" className="w-full">
|
||||||
<h3 className="text-lg font-semibold">{t('admin.userRegistration')}</h3>
|
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
||||||
<label className="flex items-center gap-2">
|
<TabsTrigger value="registration" className="flex items-center gap-2">
|
||||||
<Checkbox checked={allowRegistration} onCheckedChange={handleToggleRegistration}
|
<Users className="h-4 w-4"/>
|
||||||
disabled={regLoading}/>
|
{t('admin.general')}
|
||||||
{t('admin.allowNewAccountRegistration')}
|
</TabsTrigger>
|
||||||
</label>
|
<TabsTrigger value="oidc" className="flex items-center gap-2">
|
||||||
</div>
|
<Shield className="h-4 w-4"/>
|
||||||
</TabsContent>
|
OIDC
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="users" className="flex items-center gap-2">
|
||||||
|
<Users className="h-4 w-4"/>
|
||||||
|
{t('admin.users')}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="admins" className="flex items-center gap-2">
|
||||||
|
<Shield className="h-4 w-4"/>
|
||||||
|
{t('admin.adminManagement')}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="oidc" className="space-y-6">
|
<TabsContent value="registration" className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-semibold">{t('admin.externalAuthentication')}</h3>
|
<h3 className="text-lg font-semibold">{t('admin.userRegistration')}</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t('admin.configureExternalProvider')}</p>
|
<label className="flex items-center gap-2">
|
||||||
|
<Checkbox checked={allowRegistration} onCheckedChange={handleToggleRegistration}
|
||||||
|
disabled={regLoading}/>
|
||||||
|
{t('admin.allowNewAccountRegistration')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{oidcError && (
|
<TabsContent value="oidc" className="space-y-6">
|
||||||
<Alert variant="destructive">
|
<div className="space-y-4">
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<h3 className="text-lg font-semibold">{t('admin.externalAuthentication')}</h3>
|
||||||
<AlertDescription>{oidcError}</AlertDescription>
|
<p className="text-sm text-muted-foreground">{t('admin.configureExternalProvider')}</p>
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleOIDCConfigSubmit} className="space-y-4">
|
{oidcError && (
|
||||||
<div className="space-y-2">
|
<Alert variant="destructive">
|
||||||
<Label htmlFor="client_id">{t('admin.clientId')}</Label>
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
<Input id="client_id" value={oidcConfig.client_id}
|
<AlertDescription>{oidcError}</AlertDescription>
|
||||||
onChange={(e) => handleOIDCConfigChange('client_id', e.target.value)}
|
</Alert>
|
||||||
placeholder={t('placeholders.clientId')} required/>
|
)}
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="client_secret">{t('admin.clientSecret')}</Label>
|
|
||||||
<Input id="client_secret" type="password" value={oidcConfig.client_secret}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)}
|
|
||||||
placeholder={t('placeholders.clientSecret')} required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="authorization_url">{t('admin.authorizationUrl')}</Label>
|
|
||||||
<Input id="authorization_url" value={oidcConfig.authorization_url}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('authorization_url', e.target.value)}
|
|
||||||
placeholder={t('placeholders.authUrl')}
|
|
||||||
required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="issuer_url">{t('admin.issuerUrl')}</Label>
|
|
||||||
<Input id="issuer_url" value={oidcConfig.issuer_url}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('issuer_url', e.target.value)}
|
|
||||||
placeholder={t('placeholders.redirectUrl')} required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="token_url">{t('admin.tokenUrl')}</Label>
|
|
||||||
<Input id="token_url" value={oidcConfig.token_url}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('token_url', e.target.value)}
|
|
||||||
placeholder={t('placeholders.tokenUrl')} required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="identifier_path">{t('admin.userIdentifierPath')}</Label>
|
|
||||||
<Input id="identifier_path" value={oidcConfig.identifier_path}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('identifier_path', e.target.value)}
|
|
||||||
placeholder={t('placeholders.userIdField')} required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name_path">{t('admin.displayNamePath')}</Label>
|
|
||||||
<Input id="name_path" value={oidcConfig.name_path}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('name_path', e.target.value)}
|
|
||||||
placeholder={t('placeholders.usernameField')} required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="scopes">{t('admin.scopes')}</Label>
|
|
||||||
<Input id="scopes" value={oidcConfig.scopes}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('scopes', e.target.value)}
|
|
||||||
placeholder={t('placeholders.scopes')} required/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="userinfo_url">{t('admin.overrideUserInfoUrl')}</Label>
|
|
||||||
<Input id="userinfo_url" value={oidcConfig.userinfo_url}
|
|
||||||
onChange={(e) => handleOIDCConfigChange('userinfo_url', e.target.value)}
|
|
||||||
placeholder="https://your-provider.com/application/o/userinfo/"/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 pt-2">
|
|
||||||
<Button type="submit" className="flex-1"
|
|
||||||
disabled={oidcLoading}>{oidcLoading ? t('admin.saving') : t('admin.saveConfiguration')}</Button>
|
|
||||||
<Button type="button" variant="outline" onClick={() => setOidcConfig({
|
|
||||||
client_id: '',
|
|
||||||
client_secret: '',
|
|
||||||
issuer_url: '',
|
|
||||||
authorization_url: '',
|
|
||||||
token_url: '',
|
|
||||||
identifier_path: 'sub',
|
|
||||||
name_path: 'name',
|
|
||||||
scopes: 'openid email profile',
|
|
||||||
userinfo_url: ''
|
|
||||||
})}>{t('admin.reset')}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="users" className="space-y-6">
|
<form onSubmit={handleOIDCConfigSubmit} className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<Label htmlFor="client_id">{t('admin.clientId')}</Label>
|
||||||
<h3 className="text-lg font-semibold">{t('admin.userManagement')}</h3>
|
<Input id="client_id" value={oidcConfig.client_id}
|
||||||
<Button onClick={fetchUsers} disabled={usersLoading} variant="outline"
|
onChange={(e) => handleOIDCConfigChange('client_id', e.target.value)}
|
||||||
size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}</Button>
|
placeholder={t('placeholders.clientId')} required/>
|
||||||
</div>
|
</div>
|
||||||
{usersLoading ? (
|
{/* 🎯 Updated block for client_secret input */}
|
||||||
<div className="text-center py-8 text-muted-foreground">{t('admin.loadingUsers')}</div>
|
<div className="space-y-2">
|
||||||
) : (
|
<Label htmlFor="client_secret">{t('admin.clientSecret')}</Label>
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="relative">
|
||||||
<Table>
|
<Input
|
||||||
<TableHeader>
|
id="client_secret"
|
||||||
<TableRow>
|
// 🎯 Set input type based on showClientSecret state
|
||||||
<TableHead className="px-4">{t('admin.username')}</TableHead>
|
type={showClientSecret ? "text" : "password"}
|
||||||
<TableHead className="px-4">{t('admin.type')}</TableHead>
|
value={oidcConfig.client_secret}
|
||||||
<TableHead className="px-4">{t('admin.actions')}</TableHead>
|
onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)}
|
||||||
</TableRow>
|
placeholder={t('placeholders.clientSecret')}
|
||||||
</TableHeader>
|
required
|
||||||
<TableBody>
|
// 🎯 Add padding to the right for the button
|
||||||
{users.map((user) => (
|
className="pr-10"
|
||||||
<TableRow key={user.id}>
|
/>
|
||||||
<TableCell className="px-4 font-medium">
|
<Button
|
||||||
{user.username}
|
type="button"
|
||||||
{user.is_admin && (
|
variant="ghost"
|
||||||
<span
|
size="sm"
|
||||||
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')}</span>
|
// 🎯 Toggle the state on click
|
||||||
)}
|
onClick={() => setShowClientSecret((prev) => !prev)}
|
||||||
</TableCell>
|
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||||
<TableCell
|
>
|
||||||
className="px-4">{user.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
|
{/* 🎯 Conditionally render the correct icon */}
|
||||||
<TableCell className="px-4">
|
{showClientSecret ? (
|
||||||
<Button variant="ghost" size="sm"
|
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
||||||
onClick={() => handleDeleteUser(user.username)}
|
) : (
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||||
disabled={user.is_admin}>
|
)}
|
||||||
<Trash2 className="h-4 w-4"/>
|
</Button>
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="admins" className="space-y-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h3 className="text-lg font-semibold">{t('admin.adminManagement')}</h3>
|
|
||||||
<div className="space-y-4 p-6 border rounded-md bg-muted/50">
|
|
||||||
<h4 className="font-medium">{t('admin.makeUserAdmin')}</h4>
|
|
||||||
<form onSubmit={handleMakeUserAdmin} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="new-admin-username">{t('admin.username')}</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input id="new-admin-username" value={newAdminUsername}
|
|
||||||
onChange={(e) => setNewAdminUsername(e.target.value)}
|
|
||||||
placeholder={t('admin.enterUsernameToMakeAdmin')} required/>
|
|
||||||
<Button type="submit"
|
|
||||||
disabled={makeAdminLoading || !newAdminUsername.trim()}>{makeAdminLoading ? t('admin.adding') : t('admin.makeAdmin')}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{makeAdminError && (
|
</div>
|
||||||
<Alert variant="destructive">
|
{/* 🎯 End of updated block */}
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<div className="space-y-2">
|
||||||
<AlertDescription>{makeAdminError}</AlertDescription>
|
<Label htmlFor="authorization_url">{t('admin.authorizationUrl')}</Label>
|
||||||
</Alert>
|
<Input id="authorization_url" value={oidcConfig.authorization_url}
|
||||||
)}
|
onChange={(e) => handleOIDCConfigChange('authorization_url', e.target.value)}
|
||||||
|
placeholder={t('placeholders.authUrl')}
|
||||||
|
required/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="issuer_url">{t('admin.issuerUrl')}</Label>
|
||||||
|
<Input id="issuer_url" value={oidcConfig.issuer_url}
|
||||||
|
onChange={(e) => handleOIDCConfigChange('issuer_url', e.target.value)}
|
||||||
|
placeholder={t('placeholders.redirectUrl')} required/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="token_url">{t('admin.tokenUrl')}</Label>
|
||||||
|
<Input id="token_url" value={oidcConfig.token_url}
|
||||||
|
onChange={(e) => handleOIDCConfigChange('token_url', e.target.value)}
|
||||||
|
placeholder={t('placeholders.tokenUrl')} required/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="identifier_path">{t('admin.userIdentifierPath')}</Label>
|
||||||
|
<Input id="identifier_path" value={oidcConfig.identifier_path}
|
||||||
|
onChange={(e) => handleOIDCConfigChange('identifier_path', e.target.value)}
|
||||||
|
placeholder={t('placeholders.userIdField')} required/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name_path">{t('admin.displayNamePath')}</Label>
|
||||||
|
<Input id="name_path" value={oidcConfig.name_path}
|
||||||
|
onChange={(e) => handleOIDCConfigChange('name_path', e.target.value)}
|
||||||
|
placeholder={t('placeholders.usernameField')} required/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="scopes">{t('admin.scopes')}</Label>
|
||||||
|
<Input id="scopes" value={oidcConfig.scopes}
|
||||||
|
onChange={(e) => handleOIDCConfigChange('scopes', e.target.value)}
|
||||||
|
placeholder={t('placeholders.scopes')} required/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="userinfo_url">{t('admin.overrideUserInfoUrl')}</Label>
|
||||||
|
<Input id="userinfo_url" value={oidcConfig.userinfo_url}
|
||||||
|
onChange={(e) => handleOIDCConfigChange('userinfo_url', e.target.value)}
|
||||||
|
placeholder="https://your-provider.com/application/o/userinfo/"/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button type="submit" className="flex-1"
|
||||||
|
disabled={oidcLoading}>{oidcLoading ? t('admin.saving') : t('admin.saveConfiguration')}</Button>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOidcConfig({
|
||||||
|
client_id: '',
|
||||||
|
client_secret: '',
|
||||||
|
issuer_url: '',
|
||||||
|
authorization_url: '',
|
||||||
|
token_url: '',
|
||||||
|
identifier_path: 'sub',
|
||||||
|
name_path: 'name',
|
||||||
|
scopes: 'openid email profile',
|
||||||
|
userinfo_url: ''
|
||||||
|
})}>{t('admin.reset')}</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
</form>
|
<TabsContent value="users" className="space-y-6">
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold">{t('admin.userManagement')}</h3>
|
||||||
|
<Button onClick={fetchUsers} disabled={usersLoading} variant="outline"
|
||||||
|
size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}</Button>
|
||||||
|
</div>
|
||||||
|
{usersLoading ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">{t('admin.loadingUsers')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="border rounded-md overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="px-4">{t('admin.username')}</TableHead>
|
||||||
|
<TableHead className="px-4">{t('admin.type')}</TableHead>
|
||||||
|
<TableHead className="px-4">{t('admin.actions')}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell className="px-4 font-medium">
|
||||||
|
{user.username}
|
||||||
|
{user.is_admin && (
|
||||||
|
<span
|
||||||
|
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
className="px-4">{user.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
|
||||||
|
<TableCell className="px-4">
|
||||||
|
<Button variant="ghost" size="sm"
|
||||||
|
onClick={() => handleDeleteUser(user.username)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
disabled={user.is_admin}>
|
||||||
|
<Trash2 className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<TabsContent value="admins" className="space-y-6">
|
||||||
<h4 className="font-medium">{t('admin.currentAdmins')}</h4>
|
<div className="space-y-6">
|
||||||
<div className="border rounded-md overflow-hidden">
|
<h3 className="text-lg font-semibold">{t('admin.adminManagement')}</h3>
|
||||||
<Table>
|
<div className="space-y-4 p-6 border rounded-md bg-muted/50">
|
||||||
<TableHeader>
|
<h4 className="font-medium">{t('admin.makeUserAdmin')}</h4>
|
||||||
<TableRow>
|
<form onSubmit={handleMakeUserAdmin} className="space-y-4">
|
||||||
<TableHead className="px-4">{t('admin.username')}</TableHead>
|
<div className="space-y-2">
|
||||||
<TableHead className="px-4">{t('admin.type')}</TableHead>
|
<Label htmlFor="new-admin-username">{t('admin.username')}</Label>
|
||||||
<TableHead className="px-4">{t('admin.actions')}</TableHead>
|
<div className="flex gap-2">
|
||||||
</TableRow>
|
<Input id="new-admin-username" value={newAdminUsername}
|
||||||
</TableHeader>
|
onChange={(e) => setNewAdminUsername(e.target.value)}
|
||||||
<TableBody>
|
placeholder={t('admin.enterUsernameToMakeAdmin')} required/>
|
||||||
{users.filter(u => u.is_admin).map((admin) => (
|
<Button type="submit"
|
||||||
<TableRow key={admin.id}>
|
disabled={makeAdminLoading || !newAdminUsername.trim()}>{makeAdminLoading ? t('admin.adding') : t('admin.makeAdmin')}</Button>
|
||||||
<TableCell className="px-4 font-medium">
|
</div>
|
||||||
{admin.username}
|
</div>
|
||||||
<span
|
{makeAdminError && (
|
||||||
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')}</span>
|
<Alert variant="destructive">
|
||||||
</TableCell>
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
<TableCell
|
<AlertDescription>{makeAdminError}</AlertDescription>
|
||||||
className="px-4">{admin.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
|
</Alert>
|
||||||
<TableCell className="px-4">
|
)}
|
||||||
<Button variant="ghost" size="sm"
|
|
||||||
onClick={() => handleRemoveAdminStatus(admin.username)}
|
</form>
|
||||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
|
</div>
|
||||||
<Shield className="h-4 w-4"/>
|
|
||||||
{t('admin.removeAdminButton')}
|
<div className="space-y-4">
|
||||||
</Button>
|
<h4 className="font-medium">{t('admin.currentAdmins')}</h4>
|
||||||
</TableCell>
|
<div className="border rounded-md overflow-hidden">
|
||||||
</TableRow>
|
<Table>
|
||||||
))}
|
<TableHeader>
|
||||||
</TableBody>
|
<TableRow>
|
||||||
</Table>
|
<TableHead className="px-4">{t('admin.username')}</TableHead>
|
||||||
</div>
|
<TableHead className="px-4">{t('admin.type')}</TableHead>
|
||||||
</div>
|
<TableHead className="px-4">{t('admin.actions')}</TableHead>
|
||||||
</div>
|
</TableRow>
|
||||||
</TabsContent>
|
</TableHeader>
|
||||||
</Tabs>
|
<TableBody>
|
||||||
</div>
|
{users.filter(u => u.is_admin).map((admin) => (
|
||||||
</div>
|
<TableRow key={admin.id}>
|
||||||
</div>
|
<TableCell className="px-4 font-medium">
|
||||||
);
|
{admin.username}
|
||||||
|
<span
|
||||||
|
className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">{t('admin.adminBadge')}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
className="px-4">{admin.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
|
||||||
|
<TableCell className="px-4">
|
||||||
|
<Button variant="ghost" size="sm"
|
||||||
|
onClick={() => handleRemoveAdminStatus(admin.username)}
|
||||||
|
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
|
||||||
|
<Shield className="h-4 w-4"/>
|
||||||
|
{t('admin.removeAdminButton')}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminSettings;
|
export default AdminSettings;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
|
import {Eye, EyeOff} from "lucide-react";
|
||||||
import {cn} from "../../lib/utils.ts";
|
import {cn} from "../../lib/utils.ts";
|
||||||
import {Button} from "../../components/ui/button.tsx";
|
import {Button} from "../../components/ui/button.tsx";
|
||||||
import {Input} from "../../components/ui/input.tsx";
|
import {Input} from "../../components/ui/input.tsx";
|
||||||
@@ -64,6 +65,16 @@ export function HomepageAuth({
|
|||||||
const [signupConfirmPassword, setSignupConfirmPassword] = useState("");
|
const [signupConfirmPassword, setSignupConfirmPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [oidcLoading, setOidcLoading] = useState(false);
|
const [oidcLoading, setOidcLoading] = useState(false);
|
||||||
|
const [visibility, setVisibility] = useState({
|
||||||
|
password: false,
|
||||||
|
signupConfirm: false,
|
||||||
|
resetNew: false,
|
||||||
|
resetConfirm: false
|
||||||
|
});
|
||||||
|
const toggleVisibility = (field: keyof typeof visibility) => {
|
||||||
|
setVisibility(prev => ({ ...prev, [field]: !prev[field] }));
|
||||||
|
};
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [internalLoggedIn, setInternalLoggedIn] = useState(false);
|
const [internalLoggedIn, setInternalLoggedIn] = useState(false);
|
||||||
const [firstUser, setFirstUser] = useState(false);
|
const [firstUser, setFirstUser] = useState(false);
|
||||||
@@ -678,31 +689,53 @@ export function HomepageAuth({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="new-password">{t('auth.newPassword')}</Label>
|
<Label htmlFor="new-password">{t('auth.newPassword')}</Label>
|
||||||
<Input
|
<div className="relative">
|
||||||
id="new-password"
|
<Input
|
||||||
type="password"
|
id="new-password"
|
||||||
required
|
type={visibility.resetNew ? "text" : "password"}
|
||||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
required
|
||||||
value={newPassword}
|
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200 pr-10"
|
||||||
onChange={e => setNewPassword(e.target.value)}
|
value={newPassword}
|
||||||
disabled={resetLoading}
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
autoComplete="new-password"
|
disabled={resetLoading}
|
||||||
/>
|
autoComplete="new-password"
|
||||||
</div>
|
/>
|
||||||
<div className="flex flex-col gap-2">
|
<Button
|
||||||
<Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
|
type="button"
|
||||||
<Input
|
variant="ghost"
|
||||||
id="confirm-password"
|
size="sm"
|
||||||
type="password"
|
onClick={() => toggleVisibility('resetNew')}
|
||||||
required
|
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
>
|
||||||
value={confirmPassword}
|
{visibility.resetNew ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||||
onChange={e => setConfirmPassword(e.target.value)}
|
</Button>
|
||||||
disabled={resetLoading}
|
</div>
|
||||||
autoComplete="new-password"
|
</div>
|
||||||
/>
|
<div className="flex flex-col gap-2">
|
||||||
</div>
|
<Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="confirm-password"
|
||||||
|
type={visibility.resetConfirm ? "text" : "password"}
|
||||||
|
required
|
||||||
|
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200 pr-10"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={e => setConfirmPassword(e.target.value)}
|
||||||
|
disabled={resetLoading}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleVisibility('resetConfirm')}
|
||||||
|
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||||
|
>
|
||||||
|
{visibility.resetConfirm ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full h-11 text-base font-semibold"
|
className="w-full h-11 text-base font-semibold"
|
||||||
@@ -745,20 +778,50 @@ export function HomepageAuth({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="password">{t('common.password')}</Label>
|
<Label htmlFor="password">{t('common.password')}</Label>
|
||||||
<Input id="password" type="password" required className="h-11 text-base"
|
<div className="relative">
|
||||||
value={password} onChange={e => setPassword(e.target.value)}
|
<Input
|
||||||
disabled={loading || internalLoggedIn}/>
|
id="password"
|
||||||
|
type={visibility.password ? "text" : "password"}
|
||||||
|
required
|
||||||
|
className="h-11 text-base pr-10"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
disabled={loading || internalLoggedIn}/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleVisibility('password')}
|
||||||
|
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||||
|
>
|
||||||
|
{visibility.password ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{tab === "signup" && (
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
{tab === "signup" && (
|
||||||
<Label htmlFor="signup-confirm-password">{t('common.confirmPassword')}</Label>
|
<div className="flex flex-col gap-2">
|
||||||
<Input id="signup-confirm-password" type="password" required
|
<Label htmlFor="signup-confirm-password">{t('common.confirmPassword')}</Label>
|
||||||
className="h-11 text-base"
|
<div className="relative">
|
||||||
value={signupConfirmPassword}
|
<Input
|
||||||
onChange={e => setSignupConfirmPassword(e.target.value)}
|
id="signup-confirm-password"
|
||||||
disabled={loading || internalLoggedIn}/>
|
type={visibility.signupConfirm ? "text" : "password"}
|
||||||
</div>
|
required
|
||||||
|
className="h-11 text-base pr-10"
|
||||||
|
value={signupConfirmPassword}
|
||||||
|
onChange={e => setSignupConfirmPassword(e.target.value)}
|
||||||
|
disabled={loading || internalLoggedIn}/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleVisibility('signupConfirm')}
|
||||||
|
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||||
|
>
|
||||||
|
{visibility.signupConfirm ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" className="w-full h-11 mt-2 text-base font-semibold"
|
<Button type="submit" className="w-full h-11 mt-2 text-base font-semibold"
|
||||||
disabled={loading || internalLoggedIn}>
|
disabled={loading || internalLoggedIn}>
|
||||||
|
|||||||
Reference in New Issue
Block a user