added hide and unhide password button

This commit is contained in:
AbhilashG12
2025-09-03 21:30:38 +05:30
parent 61db35daad
commit f8e7fdfdd7
3 changed files with 595 additions and 442 deletions

69
package-lock.json generated
View File

@@ -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"

View File

@@ -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">                                 <form onSubmit={handleOIDCConfigSubmit} className="space-y-4">
<Label htmlFor="client_secret">{t('admin.clientSecret')}</Label>                                     <div className="space-y-2">
<Input id="client_secret" type="password" value={oidcConfig.client_secret}                                         <Label htmlFor="client_id">{t('admin.clientId')}</Label>
                                        <Input id="client_id" value={oidcConfig.client_id}
                                               onChange={(e) => handleOIDCConfigChange('client_id', e.target.value)}
                                               placeholder={t('placeholders.clientId')} required/>
                                    </div>
{/* 🎯 Updated block for client_secret input */}
                                    <div className="space-y-2">
                                        <Label htmlFor="client_secret">{t('admin.clientSecret')}</Label>
     <div className="relative">
<Input
id="client_secret"
// 🎯 Set input type based on showClientSecret state
type={showClientSecret ? "text" : "password"}
value={oidcConfig.client_secret}
onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)} onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)}
placeholder={t('placeholders.clientSecret')} required/> placeholder={t('placeholders.clientSecret')}
</div> required
<div className="space-y-2"> // 🎯 Add padding to the right for the button
<Label htmlFor="authorization_url">{t('admin.authorizationUrl')}</Label> className="pr-10"
<Input id="authorization_url" value={oidcConfig.authorization_url} />
onChange={(e) => handleOIDCConfigChange('authorization_url', e.target.value)} <Button
placeholder={t('placeholders.authUrl')} type="button"
required/> variant="ghost"
</div> size="sm"
<div className="space-y-2"> // 🎯 Toggle the state on click
<Label htmlFor="issuer_url">{t('admin.issuerUrl')}</Label> onClick={() => setShowClientSecret((prev) => !prev)}
<Input id="issuer_url" value={oidcConfig.issuer_url} className="absolute right-0 top-0 h-full px-3 py-2"
onChange={(e) => handleOIDCConfigChange('issuer_url', e.target.value)} >
placeholder={t('placeholders.redirectUrl')} required/> {/* 🎯 Conditionally render the correct icon */}
</div> {showClientSecret ? (
<div className="space-y-2"> <EyeOff className="h-4 w-4 text-muted-foreground" />
<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">
<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"> <Eye className="h-4 w-4 text-muted-foreground" />
<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> </Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div> </div>
)}                                     </div>
</div> {/* 🎯 End of updated block */}
</TabsContent>                                     <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="admins" className="space-y-6">                         <TabsContent value="users" className="space-y-6">
<div className="space-y-6">                             <div className="space-y-4">
<h3 className="text-lg font-semibold">{t('admin.adminManagement')}</h3>                                 <div className="flex items-center justify-between">
<div className="space-y-4 p-6 border rounded-md bg-muted/50">                                     <h3 className="text-lg font-semibold">{t('admin.userManagement')}</h3>
<h4 className="font-medium">{t('admin.makeUserAdmin')}</h4>                                     <Button onClick={fetchUsers} disabled={usersLoading} variant="outline"
<form onSubmit={handleMakeUserAdmin} className="space-y-4">                                             size="sm">{usersLoading ? t('admin.loading') : t('admin.refresh')}</Button>
<div className="space-y-2">                                 </div>
<Label htmlFor="new-admin-username">{t('admin.username')}</Label>                                 {usersLoading ? (
<div className="flex gap-2">                                     <div className="text-center py-8 text-muted-foreground">{t('admin.loadingUsers')}</div>
<Input id="new-admin-username" value={newAdminUsername}                                 ) : (
onChange={(e) => setNewAdminUsername(e.target.value)}                                     <div className="border rounded-md overflow-hidden">
placeholder={t('admin.enterUsernameToMakeAdmin')} required/>                                         <Table>
<Button type="submit"                                             <TableHeader>
disabled={makeAdminLoading || !newAdminUsername.trim()}>{makeAdminLoading ? t('admin.adding') : t('admin.makeAdmin')}</Button>                                                 <TableRow>
</div>                                                     <TableHead className="px-4">{t('admin.username')}</TableHead>
</div>                                                     <TableHead className="px-4">{t('admin.type')}</TableHead>
{makeAdminError && (                                                     <TableHead className="px-4">{t('admin.actions')}</TableHead>
<Alert variant="destructive">                                                 </TableRow>
<AlertTitle>{t('common.error')}</AlertTitle>                                             </TableHeader>
<AlertDescription>{makeAdminError}</AlertDescription>                                             <TableBody>
</Alert>                                                 {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>
</form>                         <TabsContent value="admins" className="space-y-6">
</div>                             <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>
                                        {makeAdminError && (
                                            <Alert variant="destructive">
                                                <AlertTitle>{t('common.error')}</AlertTitle>
                                                <AlertDescription>{makeAdminError}</AlertDescription>
                                            </Alert>
                                        )}
<div className="space-y-4">                                     </form>
<h4 className="font-medium">{t('admin.currentAdmins')}</h4>                                 </div>
<div className="border rounded-md overflow-hidden">
<Table>                                 <div className="space-y-4">
<TableHeader>                                     <h4 className="font-medium">{t('admin.currentAdmins')}</h4>
<TableRow>                                     <div className="border rounded-md overflow-hidden">
<TableHead className="px-4">{t('admin.username')}</TableHead>                                         <Table>
<TableHead className="px-4">{t('admin.type')}</TableHead>                                             <TableHeader>
<TableHead className="px-4">{t('admin.actions')}</TableHead>                                                 <TableRow>
</TableRow>                                                     <TableHead className="px-4">{t('admin.username')}</TableHead>
</TableHeader>                                                     <TableHead className="px-4">{t('admin.type')}</TableHead>
<TableBody>                                                     <TableHead className="px-4">{t('admin.actions')}</TableHead>
{users.filter(u => u.is_admin).map((admin) => (                                                 </TableRow>
<TableRow key={admin.id}>                                             </TableHeader>
<TableCell className="px-4 font-medium">                                             <TableBody>
{admin.username}                                                 {users.filter(u => u.is_admin).map((admin) => (
<span                                                     <TableRow key={admin.id}>
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 className="px-4 font-medium">
</TableCell>                                                             {admin.username}
<TableCell                                                             <span
className="px-4">{admin.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>                                                                 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 className="px-4">                                                         </TableCell>
<Button variant="ghost" size="sm"                                                         <TableCell
onClick={() => handleRemoveAdminStatus(admin.username)}                                                             className="px-4">{admin.is_oidc ? t('admin.external') : t('admin.local')}</TableCell>
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">                                                         <TableCell className="px-4">
<Shield className="h-4 w-4"/>                                                             <Button variant="ghost" size="sm"
{t('admin.removeAdminButton')}                                                                     onClick={() => handleRemoveAdminStatus(admin.username)}
</Button>                                                                     className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
</TableCell>                                                                 <Shield className="h-4 w-4"/>
</TableRow>                                                                 {t('admin.removeAdminButton')}
))}                                                             </Button>
</TableBody>                                                         </TableCell>
</Table>                                                     </TableRow>
</div>                                                 ))}
</div>                                             </TableBody>
</div>                                         </Table>
</TabsContent>                                     </div>
</Tabs>                                 </div>
</div>                             </div>
</div>                         </TabsContent>
</div>                     </Tabs>
);                 </div>
            </div>
        </div>
    );
} }
export default AdminSettings; export default AdminSettings;

View File

@@ -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);
@@ -679,29 +690,51 @@ export function HomepageAuth({
<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>
<div className="relative">
<Input <Input
id="new-password" id="new-password"
type="password" type={visibility.resetNew ? "text" : "password"}
required required
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200" className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200 pr-10"
value={newPassword} value={newPassword}
onChange={e => setNewPassword(e.target.value)} onChange={e => setNewPassword(e.target.value)}
disabled={resetLoading} disabled={resetLoading}
autoComplete="new-password" autoComplete="new-password"
/> />
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => toggleVisibility('resetNew')}
className="absolute right-0 top-0 h-full px-3 py-2"
>
{visibility.resetNew ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
</Button>
</div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label> <Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
<div className="relative">
<Input <Input
id="confirm-password" id="confirm-password"
type="password" type={visibility.resetConfirm ? "text" : "password"}
required required
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200" className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200 pr-10"
value={confirmPassword} value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)} onChange={e => setConfirmPassword(e.target.value)}
disabled={resetLoading} disabled={resetLoading}
autoComplete="new-password" 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> </div>
<Button <Button
type="button" type="button"
@@ -746,18 +779,48 @@ 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
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}/> 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> </div>
{tab === "signup" && ( {tab === "signup" && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="signup-confirm-password">{t('common.confirmPassword')}</Label> <Label htmlFor="signup-confirm-password">{t('common.confirmPassword')}</Label>
<Input id="signup-confirm-password" type="password" required <div className="relative">
className="h-11 text-base" <Input
id="signup-confirm-password"
type={visibility.signupConfirm ? "text" : "password"}
required
className="h-11 text-base pr-10"
value={signupConfirmPassword} value={signupConfirmPassword}
onChange={e => setSignupConfirmPassword(e.target.value)} onChange={e => setSignupConfirmPassword(e.target.value)}
disabled={loading || internalLoggedIn}/> 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> </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"