瀏覽代碼

权限修改

anhuiqiang 1 周之前
父節點
當前提交
c00978484b

+ 123 - 241
package-lock.json

@@ -1152,7 +1152,7 @@
       "version": "0.8.1",
       "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
       "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/trace-mapping": "0.3.9"
@@ -1165,7 +1165,7 @@
       "version": "0.3.9",
       "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
       "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@jridgewell/resolve-uri": "^3.0.3",
@@ -3864,28 +3864,28 @@
       "version": "1.0.12",
       "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz",
       "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@tsconfig/node12": {
       "version": "1.0.11",
       "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz",
       "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@tsconfig/node14": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz",
       "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@tsconfig/node16": {
       "version": "1.0.4",
       "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz",
       "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/@types/babel__core": {
@@ -5163,7 +5163,7 @@
       "version": "8.3.4",
       "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz",
       "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "acorn": "^8.11.0"
@@ -5172,6 +5172,15 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/agent-base": {
       "version": "7.1.4",
       "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
@@ -5377,7 +5386,7 @@
       "version": "4.1.3",
       "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz",
       "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/argparse": {
@@ -5980,6 +5989,19 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@@ -6280,6 +6302,15 @@
         "node": ">= 0.12.0"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/collect-v8-coverage": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
@@ -6584,11 +6615,23 @@
         }
       }
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/create-require": {
       "version": "1.1.1",
       "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",
       "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/cron": {
@@ -7405,7 +7448,7 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz",
       "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-      "devOptional": true,
+      "dev": true,
       "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.3.1"
@@ -8586,6 +8629,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/fraction.js": {
       "version": "5.3.4",
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -11112,7 +11164,7 @@
       "version": "1.3.6",
       "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz",
       "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "devOptional": true,
+      "dev": true,
       "license": "ISC"
     },
     "node_modules/makeerror": {
@@ -14396,6 +14448,18 @@
         "node": ">=14"
       }
     },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/stack-utils": {
       "version": "2.0.6",
       "resolved": "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -15273,7 +15337,6 @@
       "resolved": "https://registry.npmmirror.com/typeorm/-/typeorm-0.3.26.tgz",
       "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@sqltools/formatter": "^1.2.5",
         "ansis": "^3.17.0",
@@ -15376,7 +15439,6 @@
       "resolved": "https://registry.npmmirror.com/ansis/-/ansis-3.17.0.tgz",
       "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==",
       "license": "ISC",
-      "peer": true,
       "engines": {
         "node": ">=14"
       }
@@ -15390,7 +15452,6 @@
         "https://github.com/sponsors/ctavan"
       ],
       "license": "MIT",
-      "peer": true,
       "bin": {
         "uuid": "dist/esm/bin/uuid"
       }
@@ -15399,7 +15460,7 @@
       "version": "5.9.3",
       "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
-      "devOptional": true,
+      "dev": true,
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
@@ -15752,7 +15813,7 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
       "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/v8-to-istanbul": {
@@ -15835,7 +15896,6 @@
       "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz",
       "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
@@ -16185,6 +16245,24 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -16287,6 +16365,27 @@
         }
       }
     },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
@@ -16343,7 +16442,7 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz",
       "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
@@ -16439,7 +16538,8 @@
         "reflect-metadata": "^0.2.2",
         "rxjs": "^7.8.1",
         "tesseract.js": "^7.0.0",
-        "typeorm": "0.3.26"
+        "typeorm": "0.3.26",
+        "xlsx": "^0.18.5"
       },
       "devDependencies": {
         "@eslint/eslintrc": "^3.2.0",
@@ -16477,21 +16577,12 @@
       "version": "22.19.2",
       "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.2.tgz",
       "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "undici-types": "~6.21.0"
       }
     },
-    "server/node_modules/ansis": {
-      "version": "3.17.0",
-      "resolved": "https://registry.npmmirror.com/ansis/-/ansis-3.17.0.tgz",
-      "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==",
-      "license": "ISC",
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "server/node_modules/dotenv": {
       "version": "17.2.3",
       "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz",
@@ -16508,7 +16599,7 @@
       "version": "10.9.2",
       "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz",
       "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@cspotcode/source-map-support": "^0.8.0",
@@ -16548,140 +16639,13 @@
         }
       }
     },
-    "server/node_modules/typeorm": {
-      "version": "0.3.26",
-      "resolved": "https://registry.npmmirror.com/typeorm/-/typeorm-0.3.26.tgz",
-      "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==",
-      "license": "MIT",
-      "dependencies": {
-        "@sqltools/formatter": "^1.2.5",
-        "ansis": "^3.17.0",
-        "app-root-path": "^3.1.0",
-        "buffer": "^6.0.3",
-        "dayjs": "^1.11.13",
-        "debug": "^4.4.0",
-        "dedent": "^1.6.0",
-        "dotenv": "^16.4.7",
-        "glob": "^10.4.5",
-        "sha.js": "^2.4.11",
-        "sql-highlight": "^6.0.0",
-        "tslib": "^2.8.1",
-        "uuid": "^11.1.0",
-        "yargs": "^17.7.2"
-      },
-      "bin": {
-        "typeorm": "cli.js",
-        "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js",
-        "typeorm-ts-node-esm": "cli-ts-node-esm.js"
-      },
-      "engines": {
-        "node": ">=16.13.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/typeorm"
-      },
-      "peerDependencies": {
-        "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0",
-        "@sap/hana-client": "^2.14.22",
-        "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
-        "ioredis": "^5.0.4",
-        "mongodb": "^5.8.0 || ^6.0.0",
-        "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1",
-        "mysql2": "^2.2.5 || ^3.0.1",
-        "oracledb": "^6.3.0",
-        "pg": "^8.5.1",
-        "pg-native": "^3.0.0",
-        "pg-query-stream": "^4.0.0",
-        "redis": "^3.1.1 || ^4.0.0 || ^5.0.14",
-        "reflect-metadata": "^0.1.14 || ^0.2.0",
-        "sql.js": "^1.4.0",
-        "sqlite3": "^5.0.3",
-        "ts-node": "^10.7.0",
-        "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@google-cloud/spanner": {
-          "optional": true
-        },
-        "@sap/hana-client": {
-          "optional": true
-        },
-        "better-sqlite3": {
-          "optional": true
-        },
-        "ioredis": {
-          "optional": true
-        },
-        "mongodb": {
-          "optional": true
-        },
-        "mssql": {
-          "optional": true
-        },
-        "mysql2": {
-          "optional": true
-        },
-        "oracledb": {
-          "optional": true
-        },
-        "pg": {
-          "optional": true
-        },
-        "pg-native": {
-          "optional": true
-        },
-        "pg-query-stream": {
-          "optional": true
-        },
-        "redis": {
-          "optional": true
-        },
-        "sql.js": {
-          "optional": true
-        },
-        "sqlite3": {
-          "optional": true
-        },
-        "ts-node": {
-          "optional": true
-        },
-        "typeorm-aurora-data-api-driver": {
-          "optional": true
-        }
-      }
-    },
-    "server/node_modules/typeorm/node_modules/dotenv": {
-      "version": "16.6.1",
-      "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
-      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://dotenvx.com"
-      }
-    },
     "server/node_modules/undici-types": {
       "version": "6.21.0",
       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
-    "server/node_modules/uuid": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
-      "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
-      "funding": [
-        "https://github.com/sponsors/broofa",
-        "https://github.com/sponsors/ctavan"
-      ],
-      "license": "MIT",
-      "bin": {
-        "uuid": "dist/esm/bin/uuid"
-      }
-    },
     "web": {
       "version": "0.0.0",
       "dependencies": {
@@ -16712,7 +16676,7 @@
         "@vitejs/plugin-react": "^5.0.0",
         "autoprefixer": "^10.4.27",
         "postcss": "^8.5.6",
-        "tailwindcss": "^4.0.0",
+        "tailwindcss": "^4.2.1",
         "typescript": "~5.8.2",
         "vite": "^6.2.0"
       }
@@ -16727,13 +16691,6 @@
         "undici-types": "~6.21.0"
       }
     },
-    "web/node_modules/tailwindcss": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
-      "integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
-      "dev": true,
-      "license": "MIT"
-    },
     "web/node_modules/typescript": {
       "version": "5.8.3",
       "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
@@ -16754,81 +16711,6 @@
       "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
       "dev": true,
       "license": "MIT"
-    },
-    "web/node_modules/vite": {
-      "version": "6.4.1",
-      "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz",
-      "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "esbuild": "^0.25.0",
-        "fdir": "^6.4.4",
-        "picomatch": "^4.0.2",
-        "postcss": "^8.5.3",
-        "rollup": "^4.34.9",
-        "tinyglobby": "^0.2.13"
-      },
-      "bin": {
-        "vite": "bin/vite.js"
-      },
-      "engines": {
-        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/vitejs/vite?sponsor=1"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.3"
-      },
-      "peerDependencies": {
-        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
-        "jiti": ">=1.21.0",
-        "less": "*",
-        "lightningcss": "^1.21.0",
-        "sass": "*",
-        "sass-embedded": "*",
-        "stylus": "*",
-        "sugarss": "*",
-        "terser": "^5.16.0",
-        "tsx": "^4.8.1",
-        "yaml": "^2.4.2"
-      },
-      "peerDependenciesMeta": {
-        "@types/node": {
-          "optional": true
-        },
-        "jiti": {
-          "optional": true
-        },
-        "less": {
-          "optional": true
-        },
-        "lightningcss": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        },
-        "sass-embedded": {
-          "optional": true
-        },
-        "stylus": {
-          "optional": true
-        },
-        "sugarss": {
-          "optional": true
-        },
-        "terser": {
-          "optional": true
-        },
-        "tsx": {
-          "optional": true
-        },
-        "yaml": {
-          "optional": true
-        }
-      }
     }
   }
 }

+ 3 - 2
server/package.json

@@ -54,7 +54,8 @@
     "reflect-metadata": "^0.2.2",
     "rxjs": "^7.8.1",
     "tesseract.js": "^7.0.0",
-    "typeorm": "0.3.26"
+    "typeorm": "0.3.26",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@eslint/eslintrc": "^3.2.0",
@@ -104,4 +105,4 @@
     "coverageDirectory": "../coverage",
     "testEnvironment": "node"
   }
-}
+}

+ 26 - 2
server/src/admin/admin.controller.ts

@@ -1,4 +1,6 @@
-import { Controller, Get, Post, Put, Body, UseGuards, Request, Query } from '@nestjs/common';
+import { Controller, Get, Post, Put, Body, UseGuards, Request, Query, UseInterceptors, UploadedFile, Res } from '@nestjs/common';
+import { FileInterceptor } from '@nestjs/platform-express';
+import { Response } from 'express';
 import { AdminService } from './admin.service';
 import { CombinedAuthGuard } from '../auth/combined-auth.guard';
 import { RolesGuard } from '../auth/roles.guard';
@@ -17,13 +19,35 @@ export class AdminController {
         @Query('page') page?: string,
         @Query('limit') limit?: string,
     ) {
+        const isSuperAdmin = req.user.role === UserRole.SUPER_ADMIN;
         return this.adminService.getTenantUsers(
-            req.user.tenantId,
+            isSuperAdmin ? undefined : req.user.tenantId,
             page ? parseInt(page) : undefined,
             limit ? parseInt(limit) : undefined
         );
     }
 
+    @Get('users/export')
+    @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
+    async getUsersExport(@Request() req: any, @Res() res: Response) {
+        const isSuperAdmin = req.user.role === UserRole.SUPER_ADMIN;
+        const buffer = await this.adminService.exportUsers(isSuperAdmin ? undefined : req.user.tenantId);
+        res.set({
+            'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+            'Content-Disposition': 'attachment; filename="users_export.xlsx"',
+            'Content-Length': buffer.length,
+        });
+        res.end(buffer);
+    }
+
+    @Post('users/import')
+    @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
+    @UseInterceptors(FileInterceptor('file'))
+    async importUsers(@Request() req: any, @UploadedFile() file: any) {
+        const isSuperAdmin = req.user.role === UserRole.SUPER_ADMIN;
+        return this.adminService.importUsers(isSuperAdmin ? undefined : req.user.tenantId, file);
+    }
+
     @Get('settings')
     async getSettings(@Request() req: any) {
         return this.adminService.getTenantSettings(req.user.tenantId);

+ 81 - 2
server/src/admin/admin.service.ts

@@ -1,4 +1,5 @@
-import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
+import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from '@nestjs/common';
+import * as XLSX from 'xlsx';
 import { UserService } from '../user/user.service';
 import { TenantService } from '../tenant/tenant.service';
 
@@ -9,10 +10,88 @@ export class AdminService {
         private readonly tenantService: TenantService,
     ) { }
 
-    async getTenantUsers(tenantId: string, page?: number, limit?: number) {
+    async getTenantUsers(tenantId?: string, page?: number, limit?: number) {
+        if (!tenantId) {
+            return this.userService.findAll(page, limit);
+        }
         return this.userService.findByTenantId(tenantId, page, limit);
     }
 
+    async exportUsers(tenantId?: string): Promise<Buffer> {
+        const { data: users } = tenantId 
+            ? await this.userService.findByTenantId(tenantId)
+            : await this.userService.findAll();
+        
+        const worksheet = XLSX.utils.json_to_sheet(users.map(u => ({
+            Username: u.username,
+            DisplayName: u.displayName || '',
+            IsAdmin: u.isAdmin ? 'Yes' : 'No',
+            CreatedAt: u.createdAt,
+            Password: '', // Placeholder for new users
+        })));
+
+        const workbook = XLSX.utils.book_new();
+        XLSX.utils.book_append_sheet(workbook, worksheet, 'Users');
+
+        return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
+    }
+
+    async importUsers(tenantId?: string, file?: any) {
+        if (!file) throw new BadRequestException('No file uploaded');
+
+        const workbook = XLSX.read(file.buffer, { type: 'buffer' });
+        const sheetName = workbook.SheetNames[0];
+        const worksheet = workbook.Sheets[sheetName];
+        const data = XLSX.utils.sheet_to_json(worksheet) as any[];
+
+        const results = {
+            success: 0,
+            failed: 0,
+            errors: [] as string[],
+        };
+
+        for (const row of data) {
+            try {
+                const username = (row.Username || row.username)?.toString();
+                const displayName = (row.DisplayName || row.displayName || row.Name || row.name)?.toString();
+                const password = (row.Password || row.password)?.toString();
+                const isAdminStr = (row.IsAdmin || row.isAdmin || 'No').toString();
+                const isAdmin = isAdminStr.toLowerCase() === 'yes' || isAdminStr === 'true' || isAdminStr === '1';
+
+                if (!username) {
+                    throw new Error('Username is missing');
+                }
+
+                const existingUser = await this.userService.findOneByUsername(username);
+
+                if (existingUser) {
+                    await this.userService.updateUser(existingUser.id, {
+                        displayName: displayName || existingUser.displayName,
+                        password: password || undefined,
+                        // We avoid changing isAdmin status via import for security unless explicitly required
+                    });
+                } else {
+                    if (!password) {
+                        throw new Error(`Password missing for new user: ${username}`);
+                    }
+                    await this.userService.createUser(
+                        username,
+                        password,
+                        isAdmin,
+                        tenantId,
+                        displayName
+                    );
+                }
+                results.success++;
+            } catch (e: any) {
+                results.failed++;
+                results.errors.push(`${row.Username || 'Unknown'}: ${e.message}`);
+            }
+        }
+
+        return results;
+    }
+
     async getTenantSettings(tenantId: string) {
         return this.tenantService.getSettings(tenantId);
     }

+ 1 - 1
server/src/chat/chat.controller.ts

@@ -78,7 +78,7 @@ export class ChatController {
 
       if (role !== 'SUPER_ADMIN') {
         const tenantSettings = await this.tenantService.getSettings(tenantId);
-        const enabledIds = tenantSettings.enabledModelIds || [];
+        const enabledIds = tenantSettings?.enabledModelIds || [];
         // Only allow models that are enabled by the tenant admin
         models = models.filter(m => enabledIds.includes(m.id));
       }

+ 1 - 1
server/src/chat/chat.service.ts

@@ -441,7 +441,7 @@ ${instruction}`;
       const settings = await this.tenantService.getSettings(tenantId || 'default');
       const llm = new ChatOpenAI({
         apiKey: config.apiKey || 'ollama',
-        temperature: settings.temperature ?? 0.7,
+        temperature: settings?.temperature ?? 0.7,
         modelName: config.modelId,
         configuration: {
           baseURL: config.baseUrl || 'http://localhost:11434/v1',

+ 2 - 2
server/src/knowledge-base/chunk-config.service.ts

@@ -358,8 +358,8 @@ export class ChunkConfigService {
 
     if (tenantId) {
       const tenantSettings = await this.tenantService.getSettings(tenantId);
-      if (tenantSettings.chunkSize) defaultChunkSize = tenantSettings.chunkSize;
-      if (tenantSettings.chunkOverlap) defaultOverlapSize = tenantSettings.chunkOverlap;
+      if (tenantSettings?.chunkSize) defaultChunkSize = tenantSettings.chunkSize;
+      if (tenantSettings?.chunkOverlap) defaultOverlapSize = tenantSettings.chunkOverlap;
     }
 
     return {

+ 2 - 2
server/src/knowledge-base/knowledge-base.service.ts

@@ -377,7 +377,7 @@ export class KnowledgeBaseService {
     // 画像ファイルの場合はビジョンモデルを使用
     if (this.visionService.isImageFile(kb.mimetype)) {
       const settings = await this.tenantService.getSettings(tenantId || 'default');
-      const visionModelId = settings.selectedVisionId;
+      const visionModelId = settings?.selectedVisionId;
       if (visionModelId) {
         const visionModel = await this.modelConfigService.findOne(
           visionModelId,
@@ -441,7 +441,7 @@ export class KnowledgeBaseService {
 
     // Vision モデルが設定されているか確認
       const settings = await this.tenantService.getSettings(tenantId || 'default');
-    const visionModelId = settings.selectedVisionId;
+    const visionModelId = settings?.selectedVisionId;
     if (!visionModelId) {
       this.logger.warn(
         this.i18nService.getMessage('visionModelNotConfiguredFallback')

+ 10 - 10
server/src/rag/rag.service.ts

@@ -64,16 +64,16 @@ export class RagService {
     const globalSettings = await this.tenantService.getSettings(tenantId || 'default');
 
     // パラメータが明示的に渡されていない場合はグローバル設定を使用
-    const effectiveTopK = topK || globalSettings.topK || 5;
-    const effectiveVectorThreshold = vectorSimilarityThreshold !== undefined ? vectorSimilarityThreshold : (globalSettings.similarityThreshold || 0.3);
-    const effectiveRerankThreshold = rerankSimilarityThreshold !== undefined ? rerankSimilarityThreshold : (globalSettings.rerankSimilarityThreshold || 0.5);
-    const effectiveEnableRerank = enableRerank !== undefined ? enableRerank : globalSettings.enableRerank;
-    const effectiveEnableFullText = enableFullTextSearch !== undefined ? enableFullTextSearch : globalSettings.enableFullTextSearch;
-    const effectiveEmbeddingId = embeddingModelId || globalSettings.selectedEmbeddingId;
-    const effectiveRerankId = rerankModelId || globalSettings.selectedRerankId;
-    const effectiveHybridWeight = globalSettings.hybridVectorWeight ?? 0.7;
-    const effectiveEnableQueryExpansion = enableQueryExpansion !== undefined ? enableQueryExpansion : globalSettings.enableQueryExpansion;
-    const effectiveEnableHyDE = enableHyDE !== undefined ? enableHyDE : globalSettings.enableHyDE;
+    const effectiveTopK = topK || globalSettings?.topK || 5;
+    const effectiveVectorThreshold = vectorSimilarityThreshold !== undefined ? vectorSimilarityThreshold : (globalSettings?.similarityThreshold || 0.3);
+    const effectiveRerankThreshold = rerankSimilarityThreshold !== undefined ? rerankSimilarityThreshold : (globalSettings?.rerankSimilarityThreshold || 0.5);
+    const effectiveEnableRerank = enableRerank !== undefined ? enableRerank : (globalSettings?.enableRerank ?? false);
+    const effectiveEnableFullText = enableFullTextSearch !== undefined ? enableFullTextSearch : (globalSettings?.enableFullTextSearch ?? false);
+    const effectiveEmbeddingId = embeddingModelId || globalSettings?.selectedEmbeddingId;
+    const effectiveRerankId = rerankModelId || globalSettings?.selectedRerankId;
+    const effectiveHybridWeight = globalSettings?.hybridVectorWeight ?? 0.7;
+    const effectiveEnableQueryExpansion = enableQueryExpansion !== undefined ? enableQueryExpansion : (globalSettings?.enableQueryExpansion ?? false);
+    const effectiveEnableHyDE = enableHyDE !== undefined ? enableHyDE : (globalSettings?.enableHyDE ?? false);
 
     this.logger.log(
       `RAG search: query="${query}", topK=${effectiveTopK}, vectorThreshold=${effectiveVectorThreshold}, rerankThreshold=${effectiveRerankThreshold}, hybridWeight=${effectiveHybridWeight}, QueryExpansion=${effectiveEnableQueryExpansion}, HyDE=${effectiveEnableHyDE}`,

+ 24 - 5
server/src/super-admin/super-admin.controller.ts

@@ -1,4 +1,4 @@
-import { Controller, Get, Post, Put, Body, UseGuards, Param, Delete, HttpCode, Patch, ForbiddenException } from '@nestjs/common';
+import { Controller, Get, Post, Put, Body, UseGuards, Param, Delete, HttpCode, Patch, ForbiddenException, Query } from '@nestjs/common';
 import { SuperAdminService } from './super-admin.service';
 import { TenantService } from '../tenant/tenant.service';
 import { CombinedAuthGuard } from '../auth/combined-auth.guard';
@@ -16,8 +16,14 @@ export class SuperAdminController {
     ) { }
 
     @Get()
-    async getTenants() {
-        return this.superAdminService.getAllTenants();
+    async getTenants(
+        @Query('page') page?: string,
+        @Query('limit') limit?: string,
+    ) {
+        return this.superAdminService.getAllTenants(
+            page ? parseInt(page) : undefined,
+            limit ? parseInt(limit) : undefined
+        );
     }
 
     @Post()
@@ -57,8 +63,14 @@ export class SuperAdminController {
     // --- Member Management ---
 
     @Get(':tenantId/members')
-    async getMembers(@Param('tenantId') tenantId: string) {
-        return this.tenantService.getMembers(tenantId);
+    async getMembers(
+        @Param('tenantId') tenantId: string,
+        @Query('page') page?: string,
+        @Query('limit') limit?: string,
+    ) {
+        const p = page ? parseInt(page) : undefined;
+        const l = limit ? parseInt(limit) : undefined;
+        return this.tenantService.getMembers(tenantId, p, l);
     }
 
     @Post(':tenantId/members')
@@ -89,4 +101,11 @@ export class SuperAdminController {
         }
         return this.tenantService.updateMemberRole(tenantId, userId, body.role);
     }
+
+    @Get(':tenantId/members/ids')
+    async getMemberIds(
+        @Param('tenantId') tenantId: string,
+    ) {
+        return this.tenantService.getMemberIds(tenantId);
+    }
 }

+ 2 - 2
server/src/super-admin/super-admin.service.ts

@@ -11,8 +11,8 @@ export class SuperAdminService {
         private readonly userService: UserService,
     ) { }
 
-    async getAllTenants() {
-        return this.tenantService.findAll();
+    async getAllTenants(page?: number, limit?: number) {
+        return this.tenantService.findAll(page, limit);
     }
 
     async createTenant(name: string, domain?: string, adminUserId?: string, parentId?: string) {

+ 14 - 2
server/src/tenant/tenant.controller.ts

@@ -23,8 +23,13 @@ export class TenantController {
     constructor(private readonly tenantService: TenantService) { }
 
     @Get()
-    findAll() {
-        return this.tenantService.findAll();
+    findAll(
+        @Query('page') page?: string,
+        @Query('limit') limit?: string,
+    ) {
+        const p = page ? parseInt(page) : undefined;
+        const l = limit ? parseInt(limit) : undefined;
+        return this.tenantService.findAll(p, l);
     }
 
     @Get(':id')
@@ -93,4 +98,11 @@ export class TenantController {
     ) {
         await this.tenantService.removeMember(id, userId);
     }
+
+    @Get(':id/members/ids')
+    getMemberIds(
+        @Param('id') id: string,
+    ) {
+        return this.tenantService.getMemberIds(id);
+    }
 }

+ 30 - 46
server/src/tenant/tenant.service.ts

@@ -23,11 +23,21 @@ export class TenantService {
         private readonly tenantMemberRepository: Repository<TenantMember>,
     ) { }
 
-    async findAll(): Promise<Tenant[]> {
-        return this.tenantRepository.find({
-            relations: ['members', 'members.user'],
-            order: { createdAt: 'ASC' }
-        });
+    async findAll(page?: number, limit?: number): Promise<{ data: Tenant[]; total: number } | Tenant[]> {
+        const queryBuilder = this.tenantRepository.createQueryBuilder('tenant')
+            .leftJoinAndSelect('tenant.members', 'members')
+            .leftJoinAndSelect('members.user', 'user')
+            .orderBy('tenant.createdAt', 'ASC');
+
+        if (page !== undefined && limit !== undefined) {
+            const [data, total] = await queryBuilder
+                .skip((page - 1) * limit)
+                .take(limit)
+                .getManyAndCount();
+            return { data, total };
+        }
+
+        return queryBuilder.getMany();
     }
 
     async findById(id: string): Promise<Tenant> {
@@ -41,52 +51,21 @@ export class TenantService {
     }
 
     async create(name: string, domain?: string, parentId?: string, isSystem: boolean = false): Promise<Tenant> {
-        const existing = await this.findByName(name);
-        if (existing) throw new BadRequestException(`Tenant name "${name}" already exists`);
-
         const tenant = this.tenantRepository.create({ name, domain, parentId, isSystem });
-        const saved = await this.tenantRepository.save(tenant);
-
-        // Auto-create default TenantSettings
-        const setting = this.tenantSettingRepository.create({ tenantId: saved.id });
-        await this.tenantSettingRepository.save(setting);
-
-        return saved;
+        return this.tenantRepository.save(tenant);
     }
 
     async update(id: string, data: Partial<Tenant>): Promise<Tenant> {
-        const tenant = await this.findById(id);
-        if (tenant.isSystem) {
-            throw new ForbiddenException(`Cannot modify a system organization`);
-        }
-        await this.tenantRepository.save({ ...tenant, ...data });
+        await this.tenantRepository.update(id, data);
         return this.findById(id);
     }
 
     async remove(id: string): Promise<void> {
-        const tenant = await this.findById(id);
-        if (tenant.isSystem) {
-            throw new ForbiddenException(`Cannot delete a system organization`);
-        }
         await this.tenantRepository.delete(id);
     }
 
-    async getSettings(tenantId: string): Promise<TenantSetting> {
-        let setting = await this.tenantSettingRepository.findOneBy({ tenantId });
-        if (!setting) {
-            // Defensive: Check if tenant actually exists before creating settings
-            // to avoid FOREIGN KEY constraint failure if tenantId is invalid/legacy
-            const tenantExists = await this.tenantRepository.findOneBy({ id: tenantId });
-            if (!tenantExists) {
-                console.warn(`[TenantService] Attempted to get settings for non-existent tenant: ${tenantId}`);
-                // Return a transient default object without saving to DB
-                return this.tenantSettingRepository.create({ tenantId });
-            }
-
-            setting = this.tenantSettingRepository.create({ tenantId });
-            setting = await this.tenantSettingRepository.save(setting);
-        }
-        return setting;
+    async getSettings(tenantId: string): Promise<TenantSetting | null> {
+        return this.tenantSettingRepository.findOneBy({ tenantId });
     }
 
     async updateSettings(tenantId: string, data: Partial<TenantSetting>): Promise<TenantSetting> {
@@ -120,10 +99,6 @@ export class TenantService {
     }
 
     async addMember(tenantId: string, userId: string, role: string = 'USER'): Promise<TenantMember> {
-        const tenant = await this.findById(tenantId);
-        if (tenant.isSystem) {
-            throw new ForbiddenException(`Cannot manually bind members to a system organization`);
-        }
         const existing = await this.tenantMemberRepository.findOneBy({ tenantId, userId });
         if (existing) {
             existing.role = role as any;
@@ -141,9 +116,10 @@ export class TenantService {
         const queryBuilder = this.tenantMemberRepository.createQueryBuilder('member')
             .leftJoinAndSelect('member.user', 'user')
             .where('member.tenantId = :tenantId', { tenantId })
-            .select(['member', 'user.id', 'user.username', 'user.displayName', 'user.isAdmin']);
+            .select(['member', 'user.id', 'user.username', 'user.displayName', 'user.isAdmin'])
+            .orderBy('member.createdAt', 'DESC');
 
-        if (page && limit) {
+        if (page !== undefined && limit !== undefined) {
             const [data, total] = await queryBuilder
                 .skip((page - 1) * limit)
                 .take(limit)
@@ -169,4 +145,12 @@ export class TenantService {
         }
         return defaultTenant;
     }
+
+    async getMemberIds(tenantId: string): Promise<string[]> {
+        const members = await this.tenantMemberRepository.find({
+            where: { tenantId },
+            select: ['userId'],
+        });
+        return members.map(m => m.userId);
+    }
 }

+ 2 - 1
server/src/user/user.service.ts

@@ -157,7 +157,8 @@ export class UserService implements OnModuleInit {
     const user = await this.usersRepository.findOne({ where: { id: userId }, select: ['isAdmin'] });
 
     if (user?.isAdmin) {
-      const allTenants = await this.tenantService.findAll();
+      const tenantsData = await this.tenantService.findAll();
+      const allTenants = Array.isArray(tenantsData) ? tenantsData : tenantsData.data;
       const results = await Promise.all(allTenants.map(async t => {
         const settings = await this.tenantService.getSettings(t.id);
         return {

+ 6 - 4
web/components/SettingsModal.tsx

@@ -513,9 +513,11 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
                         <button onClick={() => setActiveTab('general')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'general' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
                             <Globe className="w-5 h-5" /> {t('generalSettings')}
                         </button>
-                        <button onClick={() => setActiveTab('user')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'user' ? 'bg-white text-purple-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
-                            <User className="w-5 h-5" /> {t('userManagement')}
-                        </button>
+                        {currentUser?.role === 'SUPER_ADMIN' && (
+                            <button onClick={() => setActiveTab('user')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'user' ? 'bg-white text-purple-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
+                                <User className="w-5 h-5" /> {t('userManagement')}
+                            </button>
+                        )}
                         {currentUser?.role !== 'USER' && (
                             <button onClick={() => setActiveTab('model')} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'model' ? 'bg-white text-emerald-600 shadow-sm' : 'text-slate-600 hover:bg-slate-100'}`}>
                                 <Cpu className="w-5 h-5" /> {t('modelManagement')}
@@ -540,7 +542,7 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({
                             </div>
                         )}
                         {activeTab === 'general' && renderGeneralTab()}
-                        {activeTab === 'user' && renderUserTab()}
+                        {activeTab === 'user' && currentUser?.role === 'SUPER_ADMIN' && renderUserTab()}
                         {activeTab === 'model' && currentUser?.role !== 'USER' && renderModelTab()}
                     </div>
                 </div>

+ 1 - 1
web/components/UserInfoDisplay.tsx

@@ -28,7 +28,7 @@ export const UserInfoDisplay: React.FC<UserInfoDisplayProps> = ({ currentUser })
         </div>
         <div className="flex-1 min-w-0">
           <div className="flex items-center gap-2">
-            <p className="text-sm font-medium text-white truncate">{currentUser.username}</p>
+            <p className="text-sm font-medium text-white truncate">{currentUser.displayName || currentUser.username}</p>
             {currentUser.isAdmin && (
               <span className="text-xs px-2 py-0.5 bg-orange-500/20 text-orange-300 rounded-full border border-orange-500/30">
                 {t('admin')}

+ 264 - 111
web/components/views/SettingsView.tsx

@@ -1,7 +1,48 @@
 import React, { useState, useEffect } from 'react';
 import { ModelConfig, ModelType, AppSettings, KnowledgeGroup, Tenant, TenantMember } from '../../types';
 import { useLanguage } from '../../contexts/LanguageContext';
-import { X, Plus, Trash2, Edit2, Save, Cpu, Box, Loader2, User, Users, Shield, Key, LogOut, Globe, Search, Settings as SettingsIcon, ToggleLeft, ToggleRight, Database, Sparkles, ChevronRight, ChevronDown, Lock, Building2, BookOpen, UserCircle, HardDrive, LayoutGrid } from 'lucide-react';
+import {
+  ChevronLeft,
+  ChevronRight,
+  Plus,
+  Search,
+  KeyRound,
+  Trash2,
+  Edit,
+  UserPlus,
+  Globe,
+  PlusCircle,
+  Clock,
+  ExternalLink,
+  Download,
+  Upload,
+  Building,
+  Settings as SettingsIcon,
+  Shield,
+  User,
+  MoreVertical,
+  Check,
+  ChevronDown,
+  ChevronUp,
+  Filter,
+  RefreshCcw,
+  LayoutDashboard,
+  Users,
+  Database,
+  UserCircle,
+  HardDrive,
+  LayoutGrid,
+  X,
+  Key,
+  Loader2,
+  Edit2,
+  Save,
+  Cpu,
+  BookOpen,
+  Sparkles,
+  ToggleRight,
+  ToggleLeft,
+} from "lucide-react";
 import { motion, AnimatePresence } from 'framer-motion';
 import { userService } from '../../services/userService';
 import { settingsService } from '../../services/settingsService';
@@ -46,6 +87,56 @@ const buildTenantTree = (tenants: Tenant[]): Tenant[] => {
     return roots;
 };
 
+// Moved outside to prevent re-mounting
+const Pagination: React.FC<{
+    current: number;
+    total: number;
+    pageSize: number;
+    onChange: (page: number) => void;
+}> = ({ current, total, pageSize, onChange }) => {
+    const totalPages = Math.ceil(total / pageSize);
+    if (totalPages <= 1) return null;
+
+    return (
+        <div className="flex items-center justify-center gap-2 mt-6">
+            <button
+                disabled={current === 1}
+                onClick={() => onChange(current - 1)}
+                className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
+            >
+                <ChevronDown className="w-4 h-4 rotate-90" />
+            </button>
+            <div className="flex items-center gap-1">
+                {[...Array(totalPages)].map((_, i) => {
+                    const p = i + 1;
+                    if (totalPages > 7) {
+                        if (p !== 1 && p !== totalPages && Math.abs(p - current) > 1) {
+                            if (p === 2 || p === totalPages - 1) return <span key={p} className="px-1 font-bold text-slate-300">...</span>;
+                            return null;
+                        }
+                    }
+                    return (
+                        <button
+                            key={p}
+                            onClick={() => onChange(p)}
+                            className={`w-9 h-9 flex items-center justify-center rounded-xl text-xs font-black transition-all ${current === p ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-100' : 'bg-white border border-slate-200 text-slate-500 hover:border-indigo-500 hover:text-indigo-600'}`}
+                        >
+                            {p}
+                        </button>
+                    );
+                })}
+            </div>
+            <button
+                disabled={current === totalPages}
+                onClick={() => onChange(current + 1)}
+                className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
+            >
+                <ChevronDown className="w-4 h-4 -rotate-90" />
+            </button>
+        </div>
+    );
+};
+
 export const SettingsView: React.FC<SettingsViewProps> = ({
     models,
     authToken,
@@ -99,6 +190,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     // --- Manage Members Modal State ---
     const [managingMembersTenantId, setManagingMembersTenantId] = useState<string | null>(null);
     const [tenantMembers, setTenantMembers] = useState<any[]>([]);
+    const [allMemberIds, setAllMemberIds] = useState<Set<string>>(new Set());
     const [memberUserSearch, setMemberUserSearch] = useState('');
     const [bindingRole, setBindingRole] = useState('USER');
     const [currentMemberSearch, setCurrentMemberSearch] = useState('');
@@ -106,7 +198,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     const [activeTenantManagementId, setActiveTenantManagementId] = useState<string | null>(null);
     const [memberPage, setMemberPage] = useState(1);
     const [memberTotal, setMemberTotal] = useState(0);
-    const MEMBER_PAGE_SIZE = 10;
+    const MEMBER_PAGE_SIZE = 20;
 
     const [userTotal, setUserTotal] = useState(0);
 
@@ -122,9 +214,6 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         parentId: null
     });
 
-    useEffect(() => {
-        setMemberPage(1);
-    }, [activeTenantManagementId, currentMemberSearch]);
 
 
     useEffect(() => {
@@ -134,76 +223,34 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     }, [initialTab]);
 
     useEffect(() => {
-        if (activeTab === 'user') {
-            fetchUsers();
+        if (activeTab === 'user' || activeTab === 'tenants') {
+            fetchUsers(userPage);
         }
     }, [userPage]);
 
     useEffect(() => {
         if (selectedTenantId) {
-            fetchTenantMembers(selectedTenantId);
+            fetchTenantMembers(selectedTenantId, memberPage);
+            fetchAllMemberIds(selectedTenantId);
+        } else {
+            setAllMemberIds(new Set());
         }
-    }, [memberPage, selectedTenantId]);
+    }, [selectedTenantId, memberPage]);
 
-    const Pagination: React.FC<{
-        current: number;
-        total: number;
-        pageSize: number;
-        onChange: (page: number) => void;
-    }> = ({ current, total, pageSize, onChange }) => {
-        const totalPages = Math.ceil(total / pageSize);
-        if (totalPages <= 1) return null;
-
-        return (
-            <div className="flex items-center justify-center gap-2 mt-6">
-                <button
-                    disabled={current === 1}
-                    onClick={() => onChange(current - 1)}
-                    className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
-                >
-                    <ChevronDown className="w-4 h-4 rotate-90" />
-                </button>
-                <div className="flex items-center gap-1">
-                    {[...Array(totalPages)].map((_, i) => {
-                        const p = i + 1;
-                        if (totalPages > 7) {
-                            if (p !== 1 && p !== totalPages && Math.abs(p - current) > 1) {
-                                if (p === 2 || p === totalPages - 1) return <span key={p} className="px-1 font-bold text-slate-300">...</span>;
-                                return null;
-                            }
-                        }
-                        return (
-                            <button
-                                key={p}
-                                onClick={() => onChange(p)}
-                                className={`w-9 h-9 flex items-center justify-center rounded-xl text-xs font-black transition-all ${current === p ? 'bg-indigo-600 text-white shadow-lg shadow-indigo-100' : 'bg-white border border-slate-200 text-slate-500 hover:border-indigo-500 hover:text-indigo-600'}`}
-                            >
-                                {p}
-                            </button>
-                        );
-                    })}
-                </div>
-                <button
-                    disabled={current === totalPages}
-                    onClick={() => onChange(current + 1)}
-                    className="p-2 rounded-xl bg-white border border-slate-200 text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
-                >
-                    <ChevronDown className="w-4 h-4 -rotate-90" />
-                </button>
-            </div>
-        );
-    };
-
-    // ユーザー一覧の取得(ユーザータブがアクティブな場合)
     // Data fetching on tab change
     useEffect(() => {
+        // Reset pages when switching tabs to avoid bleed-over
+        if (activeTab === 'user' || activeTab === 'tenants') {
+            setUserPage(1);
+        }
+
         if (activeTab === 'user') {
-            fetchUsers();
+            fetchUsers(1);
         } else if (activeTab === 'general') {
             fetchSettingsAndGroups();
         } else if (activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN') {
             fetchTenantsData();
-            fetchUsers(); // Ensure users are loaded for admin binding
+            fetchUsers(1); // Ensure users are loaded for admin binding
         }
 
         // Independent check for KB/Model settings to avoid being blocked by the branches above
@@ -318,10 +365,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
 
 
     // --- ユーザータブのハンドラー ---
-    const fetchUsers = async () => {
+    const fetchUsers = async (page?: number) => {
         setIsUserLoading(true);
+        const p = page || userPage;
         try {
-            const result = await userService.getUsers(userPage, USER_PAGE_SIZE);
+            const result = await userService.getUsers(p, USER_PAGE_SIZE);
             if (result && result.data) {
                 setUsers(result.data);
                 setUserTotal(result.total);
@@ -393,10 +441,22 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         }
     };
 
-    const fetchTenantMembers = async (tenantId: string) => {
+    const fetchAllMemberIds = async (tenantId: string) => {
+        try {
+            const { data } = await apiClient.get<string[]>(`/v1/tenants/${tenantId}/members/ids`);
+            if (Array.isArray(data)) {
+                setAllMemberIds(new Set(data));
+            }
+        } catch (e) {
+            console.error('Failed to fetch all member IDs:', e);
+        }
+    };
+
+    const fetchTenantMembers = async (tenantId: string, page?: number) => {
         setIsMembersLoading(true);
+        const p = page || memberPage;
         try {
-            const { data } = await apiClient.get(`/v1/tenants/${tenantId}/members?page=${memberPage}&limit=${MEMBER_PAGE_SIZE}`);
+            const { data } = await apiClient.get(`/v1/tenants/${tenantId}/members?page=${p}&limit=${MEMBER_PAGE_SIZE}`);
             if (data && data.data) {
                 setTenantMembers(data.data);
                 setMemberTotal(data.total);
@@ -411,9 +471,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         }
     };
 
-    const handleAddMember = async (tenantId: string, userId: string, role: string) => {
+    const handleAddMember = async (tenantId: string, userId: string, role: string = 'USER') => {
         try {
-            await apiClient.post(`/v1/tenants/${tenantId}/members`, { userId, role: bindingRole });
+            await apiClient.post(`/v1/tenants/${tenantId}/members`, { userId, role });
+            setAllMemberIds(prev => {
+                const next = new Set(prev);
+                next.add(userId);
+                return next;
+            });
             showSuccess(t('confirm'));
             fetchTenantMembers(tenantId);
             fetchTenantsData();
@@ -425,6 +490,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
     const handleRemoveMember = async (tenantId: string, userId: string) => {
         try {
             await apiClient.delete(`/v1/tenants/${tenantId}/members/${userId}`);
+            setAllMemberIds(prev => {
+                const next = new Set(prev);
+                next.delete(userId);
+                return next;
+            });
             showSuccess('User removed from organization');
             fetchTenantMembers(tenantId);
             fetchTenantsData();
@@ -530,6 +600,43 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
         }
     };
 
+    const handleExportUsers = async () => {
+        try {
+            const blob = await userService.exportUsers();
+            const url = window.URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = `users_${new Date().toISOString().split('T')[0]}.xlsx`;
+            document.body.appendChild(a);
+            a.click();
+            window.URL.revokeObjectURL(url);
+            document.body.removeChild(a);
+        } catch (error) {
+            console.error('Export users failed', error);
+            showError(t('exportFailed'));
+        }
+    };
+
+    const handleImportUsers = async (e: React.ChangeEvent<HTMLInputElement>) => {
+        const file = e.target.files?.[0];
+        if (!file) return;
+
+        try {
+            const result = await userService.importUsers(file);
+            showSuccess(t('importSuccess').replace('$1', (result.created + result.updated).toString()).replace('$2', result.errors.length.toString()));
+            fetchUsers();
+            if (result.errors.length > 0) {
+                console.warn('Import had errors:', result.errors);
+            }
+        } catch (error: any) {
+            console.error('Import users failed', error);
+            showError(t('importFailed') + (error.response?.data?.message ? `: ${error.response.data.message}` : ''));
+        } finally {
+            // Reset input
+            e.target.value = '';
+        }
+    };
+
     const handleSaveModel = async () => {
         if (!authToken) return;
         setIsLoading(true);
@@ -596,7 +703,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                         ) : (
                             <div className="w-5" />
                         )}
-                        <Building2 size={16} className={isSelected ? 'text-white' : 'text-slate-400'} />
+                        <Building size={16} className={isSelected ? 'text-white' : 'text-slate-400'} />
                         <span className="text-sm font-bold truncate">{tenant.name}</span>
                     </div>
 
@@ -736,13 +843,38 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                     <p className="text-xs text-slate-400 font-medium">{''}</p>
                 </div>
                 {currentUser?.role === 'SUPER_ADMIN' && (
-                    <button
-                        onClick={() => setShowAddUser(!showAddUser)}
-                        className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95"
-                    >
-                        <Plus className="w-4 h-4" />
-                        {t('addUser')}
-                    </button>
+                    <div className="flex gap-2">
+                        <button
+                            onClick={handleExportUsers}
+                            className="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 text-slate-600 text-sm font-bold rounded-2xl hover:bg-slate-50 shadow-sm transition-all active:scale-95"
+                            title={t('exportUsers')}
+                        >
+                            <Download className="w-4 h-4" />
+                            <span className="hidden sm:inline">{t('exportUsers')}</span>
+                        </button>
+                        <div className="relative">
+                            <input
+                                type="file"
+                                accept=".xlsx,.xls,.csv"
+                                onChange={handleImportUsers}
+                                className="absolute inset-0 opacity-0 cursor-pointer"
+                                title={t('importUsers')}
+                            />
+                            <button
+                                className="flex items-center gap-2 px-4 py-2.5 bg-white border border-slate-200 text-slate-600 text-sm font-bold rounded-2xl hover:bg-slate-50 shadow-sm transition-all active:scale-95"
+                            >
+                                <Upload className="w-4 h-4" />
+                                <span className="hidden sm:inline">{t('importUsers')}</span>
+                            </button>
+                        </div>
+                        <button
+                            onClick={() => setShowAddUser(!showAddUser)}
+                            className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition-all active:scale-95"
+                        >
+                            <Plus className="w-4 h-4" />
+                            {t('addUser')}
+                        </button>
+                    </div>
                 )}
             </div>
 
@@ -899,7 +1031,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                     </thead>
                     <tbody className="divide-y divide-slate-100">
                         <AnimatePresence>
-                            {users.map((user, index) => {
+                            {users.filter((u: any) => u.username !== 'admin').map((user: any, index: number) => {
                                 let IconComponent = User;
                                 let iconColors = 'bg-slate-50 text-slate-400';
                                 if (user.isAdmin) {
@@ -938,7 +1070,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                                                 key={m.tenantId}
                                                                 className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-50 text-emerald-700 text-[9px] font-black rounded-md uppercase tracking-wider border border-emerald-100"
                                                             >
-                                                                <Building2 size={8} />
+                                                                <Building size={8} />
                                                                 {m.tenant?.name || m.tenantId}
                                                             </span>
                                                         ))}
@@ -1041,8 +1173,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                     tenant={t}
                                     selectedTenantId={selectedTenantId}
                                     onSelect={(id) => {
-                                        setSelectedTenantId(id);
-                                        fetchTenantMembers(id);
+                                        if (id !== selectedTenantId) {
+                                            setSelectedTenantId(id);
+                                            setMemberPage(1);
+                                            setUserPage(1);
+                                        }
                                     }}
                                     onCreateSubtenant={(parentId) => {
                                         setNewTenant({ name: '', domain: '', parentId });
@@ -1053,7 +1188,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                             ))
                         ) : (
                             <div className="py-20 text-center">
-                                <Building2 size={32} className="mx-auto text-slate-200 mb-3" />
+                                <Building size={32} className="mx-auto text-slate-200 mb-3" />
                                 <p className="text-xs font-black text-slate-300 uppercase tracking-widest">{t('noOrganizations')}</p>
                             </div>
                         )}
@@ -1062,7 +1197,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                     <div className="p-4 bg-slate-50 border-t border-slate-100 shrink-0">
                         <div className="flex items-center gap-3 p-3 bg-white border border-slate-200 rounded-2xl shadow-sm">
                             <div className="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
-                                <Building2 size={20} />
+                                <Building size={20} />
                             </div>
                             <div>
                                 <p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('totalTenants')}</p>
@@ -1080,7 +1215,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                             <div className="p-8 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between shrink-0">
                                 <div className="flex items-center gap-4">
                                     <div className="w-14 h-14 rounded-2xl bg-white border border-slate-200 shadow-sm flex items-center justify-center text-indigo-600">
-                                        <Building2 size={28} />
+                                        <Building size={28} />
                                     </div>
                                     <div>
                                         <div className="flex items-center gap-2">
@@ -1123,7 +1258,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                     <div className="p-6 border-b border-slate-50 flex items-center justify-between shrink-0">
                                         <h4 className="text-xs font-black text-slate-400 uppercase tracking-widest">{t('orgMembers')}</h4>
                                         <span className="text-[10px] font-black px-2 py-0.5 bg-slate-100 text-slate-500 rounded-full">
-                                            {t('membersCount').replace('$1', (tenantMembers?.length || 0).toString())}
+                                            {t('membersCount').replace('$1', (memberTotal || 0).toString())}
                                         </span>
                                     </div>
                                     <div className="flex-1 overflow-y-auto p-6 scrollbar-hide">
@@ -1201,24 +1336,40 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                     <div className="flex-1 overflow-y-auto p-4 space-y-2">
                                         {users
                                             .filter(u =>
-                                                !tenantMembers?.some((m: any) => m.userId === u.id) &&
+                                                u.username !== 'admin' &&
                                                 u.username.toLowerCase().includes(userSearchQuery.toLowerCase())
                                             )
-                                            .map(u => (
-                                                <button
-                                                    key={u.id}
-                                                    onClick={() => handleAddMember(activeTenant.id, u.id, bindingRole)}
-                                                    className="w-full p-3 bg-white border border-slate-100 rounded-xl flex items-center justify-between group hover:border-indigo-500 hover:shadow-sm transition-all"
-                                                >
-                                                    <div className="flex items-center gap-2 min-w-0">
-                                                        <div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center text-slate-400 group-hover:bg-indigo-50 group-hover:text-indigo-600 transition-colors">
-                                                            <User size={14} />
+                                            .map(u => {
+                                                const isAlreadyMember = allMemberIds.has(u.id);
+                                                return (
+                                                    <button
+                                                        key={u.id}
+                                                        onClick={() => !isAlreadyMember && handleAddMember(activeTenant.id, u.id, bindingRole)}
+                                                        disabled={isAlreadyMember}
+                                                        className={`w-full p-3 border rounded-xl flex items-center justify-between group transition-all ${isAlreadyMember
+                                                            ? 'bg-slate-50 border-slate-100 cursor-not-allowed opacity-60'
+                                                            : 'bg-white border-slate-100 hover:border-indigo-500 hover:shadow-sm'
+                                                            }`}
+                                                    >
+                                                        <div className="flex items-center gap-2 min-w-0">
+                                                            <div className={`w-8 h-8 rounded-lg flex items-center justify-center transition-colors ${isAlreadyMember
+                                                                ? 'bg-slate-100 text-slate-300'
+                                                                : 'bg-slate-50 text-slate-400 group-hover:bg-indigo-50 group-hover:text-indigo-600'
+                                                                }`}>
+                                                                <User size={14} />
+                                                            </div>
+                                                            <span className={`text-[13px] font-bold truncate ${isAlreadyMember ? 'text-slate-400' : 'text-slate-700'}`}>
+                                                                {u.username}
+                                                            </span>
                                                         </div>
-                                                        <span className="text-[13px] font-bold text-slate-700 truncate">{u.username}</span>
-                                                    </div>
-                                                    <Plus size={14} className="text-slate-300 group-hover:text-indigo-600" />
-                                                </button>
-                                            ))
+                                                        {isAlreadyMember ? (
+                                                            <Check size={14} className="text-emerald-500" />
+                                                        ) : (
+                                                            <Plus size={14} className="text-slate-300 group-hover:text-indigo-600" />
+                                                        )}
+                                                    </button>
+                                                );
+                                            })
                                         }
                                         <Pagination
                                             current={userPage}
@@ -1276,7 +1427,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                     ) : (
                         <div className="flex-1 flex flex-col items-center justify-center p-12 text-center bg-slate-50/30">
                             <div className="w-24 h-24 rounded-[2rem] bg-indigo-50 flex items-center justify-center text-indigo-400 mb-6 shadow-xl shadow-indigo-100/50">
-                                <Building2 size={48} />
+                                <Building size={48} />
                             </div>
                             <h2 className="text-2xl font-black text-slate-900 tracking-tight mb-2">{t('selectOrg')}</h2>
                             <p className="text-sm text-slate-400 max-w-xs font-medium leading-relaxed">{t('selectOrgDesc')}</p>
@@ -1879,16 +2030,18 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                         <SettingsIcon size={18} />
                         {t('generalSettings')}
                     </button>
-                    {(currentUser?.role === 'TENANT_ADMIN' || currentUser?.role === 'SUPER_ADMIN' || isAdmin) && (
+                    {currentUser?.role === 'SUPER_ADMIN' && (
+                        <button
+                            onClick={() => setActiveTab('user')}
+                            className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'user' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
+                                }`}
+                        >
+                            <UserCircle size={18} />
+                            {t('userManagement')}
+                        </button>
+                    )}
+                    {isAdmin && (
                         <>
-                            <button
-                                onClick={() => setActiveTab('user')}
-                                className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'user' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
-                                    }`}
-                            >
-                                <UserCircle size={18} />
-                                {t('userManagement')}
-                            </button>
                             <button
                                 onClick={() => setActiveTab('model')}
                                 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'model' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
@@ -1960,9 +2113,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
                                 transition={{ duration: 0.3 }}
                             >
                                 {activeTab === 'general' && renderGeneralTab()}
-                                {activeTab === 'user' && renderUserTab()}
-                                {activeTab === 'model' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderModelTab()}
-                                {activeTab === 'knowledge_base' && (isAdmin || currentUser?.role === 'SUPER_ADMIN') && renderKnowledgeBaseTab()}
+                                {activeTab === 'user' && currentUser?.role === 'SUPER_ADMIN' && renderUserTab()}
+                                {activeTab === 'model' && isAdmin && renderModelTab()}
+                                {activeTab === 'knowledge_base' && isAdmin && renderKnowledgeBaseTab()}
                                 {activeTab === 'tenants' && currentUser?.role === 'SUPER_ADMIN' && renderTenantsTab()}
                             </motion.div>
                         </AnimatePresence>

+ 1 - 1
web/index.tsx

@@ -47,7 +47,7 @@ function OverviewPage() {
     <div className="p-8 bg-white rounded-2xl shadow-sm border border-slate-100">
       <h1 className="text-2xl font-bold text-slate-900">Welcome back 👋</h1>
       <p className="mt-2 text-slate-500">
-        Signed in as <span className="font-semibold">{user?.username}</span>{' '}
+        Signed in as <span className="font-semibold">{user?.displayName || user?.username}</span>{' '}
         · role <span className="font-semibold text-blue-600">{user?.role?.replace(/_/g, ' ')}</span>
       </p>
     </div>

+ 46 - 32
web/services/apiClient.ts

@@ -28,6 +28,22 @@ class ApiClient {
     };
   }
 
+  private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
+    const text = await response.text();
+    let data: any;
+    try {
+      data = text ? JSON.parse(text) : null;
+    } catch (e) {
+      data = null;
+    }
+
+    if (!response.ok) {
+      throw new Error(data?.message || text || 'Request failed');
+    }
+
+    return { data: data as T, status: response.status };
+  }
+
   // 新しい API 呼び出し方法、{ data, status } を返す
   async get<T = any>(url: string): Promise<ApiResponse<T>> {
     const response = await fetch(`${this.baseURL}${url}`, {
@@ -35,13 +51,7 @@ class ApiClient {
       headers: this.getAuthHeaders(),
     });
 
-    if (!response.ok) {
-      const errorData = await response.json();
-      throw new Error(errorData.message || 'Request failed');
-    }
-
-    const data = await response.json();
-    return { data, status: response.status };
+    return this.handleResponse<T>(response);
   }
 
   async post<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
@@ -51,13 +61,7 @@ class ApiClient {
       body: body ? JSON.stringify(body) : undefined,
     });
 
-    if (!response.ok) {
-      const errorData = await response.json();
-      throw new Error(errorData.message || 'Request failed');
-    }
-
-    const data = await response.json();
-    return { data, status: response.status };
+    return this.handleResponse<T>(response);
   }
 
   async put<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
@@ -67,13 +71,7 @@ class ApiClient {
       body: body ? JSON.stringify(body) : undefined,
     });
 
-    if (!response.ok) {
-      const errorData = await response.json();
-      throw new Error(errorData.message || 'Request failed');
-    }
-
-    const data = await response.json();
-    return { data, status: response.status };
+    return this.handleResponse<T>(response);
   }
 
   async patch<T = any>(url: string, body?: any): Promise<ApiResponse<T>> {
@@ -83,13 +81,7 @@ class ApiClient {
       body: body ? JSON.stringify(body) : undefined,
     });
 
-    if (!response.ok) {
-      const errorData = await response.json();
-      throw new Error(errorData.message || 'Request failed');
-    }
-
-    const data = await response.json();
-    return { data, status: response.status };
+    return this.handleResponse<T>(response);
   }
 
   async delete<T = any>(url: string): Promise<ApiResponse<T>> {
@@ -98,15 +90,37 @@ class ApiClient {
       headers: this.getAuthHeaders(),
     });
 
+    return this.handleResponse<T>(response);
+  }
+  
+  // New methods for special formats
+  async getBlob(url: string): Promise<Blob> {
+    const response = await fetch(`${this.baseURL}${url}`, {
+      method: 'GET',
+      headers: this.getAuthHeaders(),
+    });
+
     if (!response.ok) {
-      const errorData = await response.json();
-      throw new Error(errorData.message || 'Request failed');
+      throw new Error('Request failed');
     }
 
-    const data = await response.json();
-    return { data, status: response.status };
+    return await response.blob();
   }
 
+  async postMultipart<T = any>(url: string, formData: FormData): Promise<ApiResponse<T>> {
+    const headers = this.getAuthHeaders();
+    // Remove Content-Type to let the browser set it with the correct boundary
+    delete headers['Content-Type'];
+
+    const response = await fetch(`${this.baseURL}${url}`, {
+      method: 'POST',
+      headers,
+      body: formData,
+    });
+
+    return this.handleResponse<T>(response);
+  }
+  
   // Legacy compatibility method — returns raw Response for streaming and other special cases
   async request(path: string, options: RequestInit = {}): Promise<Response> {
     const apiKey = localStorage.getItem('kb_api_key');

+ 11 - 0
web/services/userService.ts

@@ -44,4 +44,15 @@ export const userService = {
     });
     return data;
   },
+  
+  async exportUsers(): Promise<Blob> {
+    return await apiClient.getBlob('/v1/admin/users/export');
+  },
+
+  async importUsers(file: File): Promise<any> {
+    const formData = new FormData();
+    formData.append('file', file);
+    const { data } = await apiClient.postMultipart('/v1/admin/users/import', formData);
+    return data;
+  },
 };

+ 1 - 1
web/src/components/layouts/WorkspaceLayout.tsx

@@ -188,7 +188,7 @@ const WorkspaceLayout: React.FC = () => {
                         </div>
                         <div className="flex-1 min-w-0">
                             <div className="flex items-center gap-2 mb-0.5">
-                                <p className="text-sm font-semibold text-slate-900 truncate">{user?.username}</p>
+                                <p className="text-sm font-semibold text-slate-900 truncate">{user?.displayName || user?.username}</p>
                                 <span className="px-1.5 py-0.5 text-[9px] font-black bg-blue-50 text-blue-600 rounded-md border border-blue-100 uppercase tracking-tighter">
                                     {activeTenant?.role?.replace('_', ' ') || user?.role?.replace('_', ' ') || 'USER'}
                                 </span>

+ 2 - 2
web/src/pages/workspace/SettingsPage.tsx

@@ -9,7 +9,7 @@ interface SettingsPageProps {
 }
 
 export default function SettingsPage({ initialTab }: SettingsPageProps) {
-    const { apiKey, user } = useAuth();
+    const { apiKey, user, activeTenant } = useAuth();
     const [modelConfigs, setModelConfigs] = useState<ModelConfig[]>(DEFAULT_MODELS);
 
     const fetchModels = useCallback(async () => {
@@ -41,7 +41,7 @@ export default function SettingsPage({ initialTab }: SettingsPageProps) {
             models={modelConfigs}
             onUpdateModels={handleUpdateModels}
             authToken={apiKey}
-            isAdmin={user?.role === 'TENANT_ADMIN' || user?.role === 'SUPER_ADMIN'}
+            isAdmin={user?.role === 'SUPER_ADMIN' || activeTenant?.role === 'TENANT_ADMIN'}
             currentUser={user}
             initialTab={initialTab}
         />

+ 21 - 7
web/utils/translations.ts

@@ -232,6 +232,11 @@ export const translations = {
     userDeletedSuccessfully: "用户已成功删除",
     createdAt: "创建时间",
     newChat: "新建对话",
+    exportUsers: "导出用户",
+    importUsers: "导入用户",
+    importSuccess: "完成导入: 成功 $1, 失败 $2",
+    importFailed: "导入用户失败",
+    exportFailed: "导出用户失败",
 
     // Knowledge Base View
     kbManagement: "知识库管理",
@@ -610,10 +615,10 @@ export const translations = {
     loadMore: "加载更多",
     loadingHistoriesFailed: "加载搜索历史失败",
     generalSettingsSubtitle: "管理您的应用程序首选项。",
-    userManagementSubtitle: "管理访问权限和帐户。",
+    userManagementSubtitle: "查看和管理系统用户信息。",
     modelManagementSubtitle: "配置全局 AI 模型。",
     kbSettingsSubtitle: "索引和聊天参数的技术配置。",
-    tenantsSubtitle: "全局系统概览。",
+    tenantsSubtitle: "管理组织架构、成员及其权限。",
 
     allNotes: "所有笔记",
     filterNotesPlaceholder: "筛选笔记...",
@@ -1114,6 +1119,11 @@ export const translations = {
     enterNewPassword: "Please enter new password",
     createdAt: "Created At",
     newChat: "New Chat",
+    exportUsers: "Export Users",
+    importUsers: "Import Users",
+    importSuccess: "Import completed: Success $1, Failed $2",
+    importFailed: "Failed to import users",
+    exportFailed: "Failed to export users",
 
     // Group Selection Drawer
     selectKnowledgeGroups: "Select Knowledge Groups",
@@ -1432,10 +1442,10 @@ export const translations = {
     loadingHistoriesFailed: "Failed to load search history",
     supportedFormatsInfo: "Supports documents, images and code formats",
     generalSettingsSubtitle: "Manage your application preferences.",
-    userManagementSubtitle: "Manage access and accounts.",
+    userManagementSubtitle: "View and manage system user information.",
     modelManagementSubtitle: "Configure global AI models.",
     kbSettingsSubtitle: "Technical configuration for indexing and chat parameters.",
-    tenantsSubtitle: "Global system overview.",
+    tenantsSubtitle: "Manage organization structure and members.",
 
     allNotes: "All Notes",
     filterNotesPlaceholder: "Filter notes...",
@@ -2060,7 +2070,11 @@ export const translations = {
     validationFailedMsg: "validationFailedMsg",
 
     // Sidebar
-    navChat: "チャット",
+    exportUsers: "ユーザーをエクスポート",
+    importUsers: "ユーザーをインポート",
+    importSuccess: "インポート完了: 成功 $1, 失敗 $2",
+    importFailed: "ユーザーのインポートに失敗しました",
+    exportFailed: "ユーザーのエクスポートに失敗しました",
     navCoach: "コーチ",
     navKnowledge: "ナレッジベース",
     navKnowledgeGroups: "ナレッジグループ",
@@ -2345,10 +2359,10 @@ export const translations = {
     browseManageFiles: "このグループ内のファイルとメモを閲覧・管理します。",
     filterGroupFiles: "名前でグループ内のファイルを検索...",
     generalSettingsSubtitle: "アプリケーションの設定を管理します。",
-    userManagementSubtitle: "アクセス権限とアカウントを管理します。",
+    userManagementSubtitle: "システムユーザー情報の表示と管理。",
     modelManagementSubtitle: "グローバルなAIモデルを設定します。",
     kbSettingsSubtitle: "インデックス作成とチャットパラメータの技術設定。",
-    tenantsSubtitle: "グローバルシステムの概要。",
+    tenantsSubtitle: "組織構造、メンバー、およびその権限の管理。",
     allNotes: "すべてのノート",
     filterNotesPlaceholder: "ノートをフィルタリング...",
     startWritingPlaceholder: "書き始める...",

+ 104 - 26
yarn.lock

@@ -320,6 +320,9 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.27.1"
 
+"@babel/runtime@^7.21.0":
+  version "7.28.6"
+
 "@babel/runtime@^7.28.4":
   version "7.28.4"
   resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz"
@@ -2001,21 +2004,21 @@
   dependencies:
     "@types/express" "*"
 
-"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@>=18":
+"@types/node@*", "@types/node@>=18":
   version "25.0.0"
   resolved "https://registry.npmmirror.com/@types/node/-/node-25.0.0.tgz"
   integrity sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==
   dependencies:
     undici-types "~7.16.0"
 
-"@types/node@^22.10.7":
+"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.14.0":
   version "22.19.2"
   resolved "https://registry.npmmirror.com/@types/node/-/node-22.19.2.tgz"
   integrity sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==
   dependencies:
     undici-types "~6.21.0"
 
-"@types/node@^22.14.0":
+"@types/node@^22.10.7":
   version "22.19.2"
   resolved "https://registry.npmmirror.com/@types/node/-/node-22.19.2.tgz"
   integrity sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==
@@ -2449,6 +2452,11 @@ acorn-walk@^8.1.1:
   resolved "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz"
   integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
 
+adler-32@~1.3.0:
+  version "1.3.1"
+  resolved "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz"
+  integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
+
 agent-base@^7.1.2:
   version "7.1.4"
   resolved "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz"
@@ -2939,6 +2947,14 @@ ccount@^2.0.0:
   resolved "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz"
   integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
 
+cfb@~1.2.1:
+  version "1.2.2"
+  resolved "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz"
+  integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
+  dependencies:
+    adler-32 "~1.3.0"
+    crc-32 "~1.2.0"
+
 chalk-template@^0.4.0:
   version "0.4.0"
   resolved "https://registry.npmmirror.com/chalk-template/-/chalk-template-0.4.0.tgz"
@@ -3094,6 +3110,11 @@ co@^4.6.0:
   resolved "https://registry.npmmirror.com/co/-/co-4.6.0.tgz"
   integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
 
+codepage@~1.15.0:
+  version "1.15.0"
+  resolved "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz"
+  integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
+
 collect-v8-coverage@^1.0.2:
   version "1.0.3"
   resolved "https://registry.npmmirror.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz"
@@ -3192,6 +3213,21 @@ concat-stream@^2.0.0:
     readable-stream "^3.0.2"
     typedarray "^0.0.6"
 
+concurrently@^8.2.2:
+  version "8.2.2"
+  resolved "https://registry.npmmirror.com/concurrently/-/concurrently-8.2.2.tgz"
+  integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==
+  dependencies:
+    chalk "^4.1.2"
+    date-fns "^2.30.0"
+    lodash "^4.17.21"
+    rxjs "^7.8.1"
+    shell-quote "^1.8.1"
+    spawn-command "0.0.2"
+    supports-color "^8.1.1"
+    tree-kill "^1.2.2"
+    yargs "^17.7.2"
+
 confbox@^0.1.8:
   version "0.1.8"
   resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz"
@@ -3281,6 +3317,11 @@ cosmiconfig@^8.2.0:
     parse-json "^5.2.0"
     path-type "^4.0.0"
 
+crc-32@~1.2.0, crc-32@~1.2.1:
+  version "1.2.2"
+  resolved "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz"
+  integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
+
 create-require@^1.1.0:
   version "1.1.1"
   resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz"
@@ -3623,6 +3664,13 @@ data-uri-to-buffer@^4.0.0:
   resolved "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz"
   integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
 
+date-fns@^2.30.0:
+  version "2.30.0"
+  resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz"
+  integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+  dependencies:
+    "@babel/runtime" "^7.21.0"
+
 dayjs@^1.11.13, dayjs@^1.11.18:
   version "1.11.19"
   resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz"
@@ -4372,6 +4420,11 @@ forwarded@0.2.0:
   resolved "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz"
   integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
 
+frac@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz"
+  integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
+
 fraction.js@^5.3.4:
   version "5.3.4"
   resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz"
@@ -7609,6 +7662,11 @@ shebang-regex@^3.0.0:
   resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
+shell-quote@^1.8.1:
+  version "1.8.3"
+  resolved "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz"
+  integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
+
 side-channel-list@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz"
@@ -7729,6 +7787,11 @@ space-separated-tokens@^2.0.0:
   resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
   integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
 
+spawn-command@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.npmmirror.com/spawn-command/-/spawn-command-0.0.2.tgz"
+  integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==
+
 sprintf-js@~1.0.2:
   version "1.0.3"
   resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz"
@@ -7739,6 +7802,13 @@ sql-highlight@^6.0.0:
   resolved "https://registry.npmmirror.com/sql-highlight/-/sql-highlight-6.1.0.tgz"
   integrity sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==
 
+ssf@~0.11.2:
+  version "0.11.2"
+  resolved "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz"
+  integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
+  dependencies:
+    frac "~1.1.2"
+
 stack-utils@^2.0.6:
   version "2.0.6"
   resolved "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz"
@@ -7947,7 +8017,7 @@ tailwind-merge@^3.5.0:
   resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz"
   integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
 
-tailwindcss@^4.0.0:
+tailwindcss@^4.2.1:
   version "4.0.0"
   resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz"
   integrity sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==
@@ -8093,6 +8163,11 @@ tr46@~0.0.3:
   resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
   integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
 
+tree-kill@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz"
+  integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
+
 trim-lines@^3.0.0:
   version "3.0.1"
   resolved "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz"
@@ -8139,7 +8214,7 @@ ts-loader@^9.5.2:
     semver "^7.3.4"
     source-map "^0.7.4"
 
-ts-node@^10.7.0, ts-node@^10.9.2:
+ts-node@^10.9.2:
   version "10.9.2"
   resolved "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz"
   integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
@@ -8247,27 +8322,7 @@ typedarray@^0.0.6:
   resolved "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz"
   integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
 
-typeorm@^0.3.0:
-  version "0.3.26"
-  resolved "https://registry.npmmirror.com/typeorm/-/typeorm-0.3.26.tgz"
-  integrity sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==
-  dependencies:
-    "@sqltools/formatter" "^1.2.5"
-    ansis "^3.17.0"
-    app-root-path "^3.1.0"
-    buffer "^6.0.3"
-    dayjs "^1.11.13"
-    debug "^4.4.0"
-    dedent "^1.6.0"
-    dotenv "^16.4.7"
-    glob "^10.4.5"
-    sha.js "^2.4.11"
-    sql-highlight "^6.0.0"
-    tslib "^2.8.1"
-    uuid "^11.1.0"
-    yargs "^17.7.2"
-
-typeorm@0.3.26:
+typeorm@^0.3.0, typeorm@0.3.26:
   version "0.3.26"
   resolved "https://registry.npmmirror.com/typeorm/-/typeorm-0.3.26.tgz"
   integrity sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==
@@ -8746,11 +8801,21 @@ which@^2.0.1:
   dependencies:
     isexe "^2.0.0"
 
+wmf@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz"
+  integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
+
 word-wrap@^1.2.5:
   version "1.2.5"
   resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz"
   integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
 
+word@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.npmmirror.com/word/-/word-0.3.0.tgz"
+  integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
+
 wordwrap@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz"
@@ -8815,6 +8880,19 @@ ws@^8.13.0, ws@^8.18.0:
   resolved "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz"
   integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
 
+xlsx@^0.18.5:
+  version "0.18.5"
+  resolved "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz"
+  integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
+  dependencies:
+    adler-32 "~1.3.0"
+    cfb "~1.2.1"
+    codepage "~1.15.0"
+    crc-32 "~1.2.1"
+    ssf "~0.11.2"
+    wmf "~1.0.1"
+    word "~0.3.0"
+
 xtend@^4.0.2:
   version "4.0.2"
   resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz"