From e497898ef14fe10b8d26433da8576cafd474d9d7 Mon Sep 17 00:00:00 2001 From: Lawrence Liu Date: Thu, 13 Nov 2025 01:37:24 +0800 Subject: [PATCH] feat: add whether to enable self registration toggle (#760) * refactor(frontend): extract RegistrationDisabled as reusable component - Create RegistrationDisabled component with i18n support - Add registrationClosed and registrationClosedMessage translations - Replace inline JSX in App.tsx with new component - Improve code maintainability and reusability - Add hover effect to back button for better UX * fix(frontend): add registration toggle to LoginModal component - Add useSystemConfig hook to LoginModal - Conditionally render registration button based on registration_enabled config - Ensures consistency with HeaderBar and LoginPage registration controls - Completes registration toggle feature implementation across all entry points * feat(frontend): add registration toggle UI support - Add registration disabled page in App.tsx when registration is closed - Hide registration link in LoginPage when registration is disabled - Add registration_enabled field to SystemConfig interface - Frontend conditionally shows/hides registration UI based on backend config * feat: add registration toggle feature Add system-level registration enable/disable control: - Add registration_enabled config to system_config table (default: true) - Add registration check in handleRegister API endpoint - Expose registration_enabled status in /api/config endpoint - Frontend can use this config to conditionally show/hide registration UI This allows administrators to control user registration without code changes. * fix(frontend): add registration toggle to HeaderBar and RegisterPage - Add useSystemConfig hook and registrationEnabled check to HeaderBar - Conditionally show/hide signup buttons in both desktop and mobile views - Add registration check to RegisterPage to show RegistrationDisabled component - This completes the registration toggle feature across all UI components * test(frontend): add comprehensive unit tests for registration toggle feature - Add RegistrationDisabled component tests (rendering, navigation, styling) - Add registrationToggle logic tests (config handling, edge cases, multi-location consistency) - Configure Vitest with jsdom environment for React component testing - All 80 tests passing (9 new tests for RegistrationDisabled + 21 for toggle logic) --- api/server.go | 23 +- config.json.example | 1 + config/database.go | 1 + web/package-lock.json | 706 ------------------ web/src/components/HeaderBar.tsx | 49 +- web/src/components/LoginPage.tsx | 5 +- web/src/components/RegisterPage.tsx | 10 +- .../components/RegistrationDisabled.test.tsx | 103 +++ web/src/components/RegistrationDisabled.tsx | 39 + web/src/components/landing/LoginModal.tsx | 40 +- web/src/i18n/translations.ts | 5 + web/src/lib/config.ts | 1 + web/src/lib/registrationToggle.test.ts | 204 +++++ 13 files changed, 437 insertions(+), 750 deletions(-) create mode 100644 web/src/components/RegistrationDisabled.test.tsx create mode 100644 web/src/components/RegistrationDisabled.tsx create mode 100644 web/src/lib/registrationToggle.test.ts diff --git a/api/server.go b/api/server.go index b3f67f11..27556d05 100644 --- a/api/server.go +++ b/api/server.go @@ -197,11 +197,18 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { betaModeStr, _ := s.database.GetSystemConfig("beta_mode") betaMode := betaModeStr == "true" + regEnabledStr, err := s.database.GetSystemConfig("registration_enabled") + registrationEnabled := true + if err == nil { + registrationEnabled = strings.ToLower(regEnabledStr) != "false" + } + c.JSON(http.StatusOK, gin.H{ - "beta_mode": betaMode, - "default_coins": defaultCoins, - "btc_eth_leverage": btcEthLeverage, - "altcoin_leverage": altcoinLeverage, + "beta_mode": betaMode, + "default_coins": defaultCoins, + "btc_eth_leverage": btcEthLeverage, + "altcoin_leverage": altcoinLeverage, + "registration_enabled": registrationEnabled, }) } @@ -1704,6 +1711,14 @@ func (s *Server) handleLogout(c *gin.Context) { // handleRegister 处理用户注册请求 func (s *Server) handleRegister(c *gin.Context) { + regEnabled := true + if regStr, err := s.database.GetSystemConfig("registration_enabled"); err == nil { + regEnabled = strings.ToLower(regStr) != "false" + } + if !regEnabled { + c.JSON(http.StatusForbidden, gin.H{"error": "注册已关闭"}) + return + } var req struct { Email string `json:"email" binding:"required,email"` diff --git a/config.json.example b/config.json.example index 1c7406d0..331fbc54 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,6 @@ { "beta_mode": false, + "registration_enabled": true, "leverage": { "btc_eth_leverage": 5, "altcoin_leverage": 5 diff --git a/config/database.go b/config/database.go index 5977ae3c..8d3e4ff8 100644 --- a/config/database.go +++ b/config/database.go @@ -324,6 +324,7 @@ func (d *Database) initDefaultData() error { "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 "altcoin_leverage": "5", // 山寨币杠杆倍数 "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 + "registration_enabled": "true", // 默认允许注册 } for key, value := range systemConfigs { diff --git a/web/package-lock.json b/web/package-lock.json index 85f06c30..7048f643 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1160,192 +1160,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/ansi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", - "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", - "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", - "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@inquirer/ansi": "^1.0.1", - "@inquirer/figures": "^1.0.14", - "@inquirer/type": "^3.0.9", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@inquirer/core/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@inquirer/core/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/core/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", - "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", - "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1408,26 +1222,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mswjs/interceptors": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", - "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1463,37 +1257,6 @@ "node": ">= 8" } }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2406,15 +2169,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", @@ -3469,126 +3223,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5055,18 +4689,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -5228,18 +4850,6 @@ "dev": true, "license": "MIT" }, - "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -5333,15 +4943,6 @@ "node": ">= 0.4" } }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -5737,15 +5338,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6515,104 +6107,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/msw": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.6.tgz", - "integrity": "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.40.0", - "@open-draft/deferred-promise": "^2.2.0", - "@types/statuses": "^2.0.4", - "cookie": "^1.0.2", - "graphql": "^16.8.1", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.7.0", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.0", - "type-fest": "^4.26.1", - "until-async": "^3.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/msw/node_modules/tldts": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", - "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tldts-core": "^7.0.17" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/msw/node_modules/tldts-core": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", - "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/msw/node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -6842,15 +6336,6 @@ "node": ">= 0.8.0" } }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -6980,15 +6465,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -7645,18 +7121,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -7704,15 +7168,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rettime": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", - "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -8123,18 +7578,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -8156,15 +7599,6 @@ "node": ">= 0.4" } }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -8735,21 +8169,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -8860,18 +8279,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/until-async": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/kettanaito" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -10510,18 +9917,6 @@ "dev": true, "license": "MIT" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -10541,92 +9936,6 @@ "node": ">= 14.6" } }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -10640,21 +9949,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", diff --git a/web/src/components/HeaderBar.tsx b/web/src/components/HeaderBar.tsx index 3c1870c4..7d3bd1c6 100644 --- a/web/src/components/HeaderBar.tsx +++ b/web/src/components/HeaderBar.tsx @@ -4,6 +4,7 @@ import { motion } from 'framer-motion' import { Menu, X, ChevronDown } from 'lucide-react' import { t, type Language } from '../i18n/translations' import { Container } from './Container' +import { useSystemConfig } from '../hooks/useSystemConfig' interface HeaderBarProps { onLoginClick?: () => void @@ -33,6 +34,8 @@ export default function HeaderBar({ const [userDropdownOpen, setUserDropdownOpen] = useState(false) const dropdownRef = useRef(null) const userDropdownRef = useRef(null) + const { config: systemConfig } = useSystemConfig() + const registrationEnabled = systemConfig?.registration_enabled !== false // Close dropdown when clicking outside useEffect(() => { @@ -464,16 +467,18 @@ export default function HeaderBar({ > {t('signIn', language)} - - {t('signUp', language)} - + {registrationEnabled && ( + + {t('signUp', language)} + + )} ) )} @@ -901,17 +906,19 @@ export default function HeaderBar({ > {t('signIn', language)} - setMobileMenuOpen(false)} - > - {t('signUp', language)} - + {registrationEnabled && ( + setMobileMenuOpen(false)} + > + {t('signUp', language)} + + )} )} diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index e498d91d..65c0aa02 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -6,6 +6,7 @@ import { t } from '../i18n/translations' import { Eye, EyeOff } from 'lucide-react' import { Input } from './ui/input' import { toast } from 'sonner' +import { useSystemConfig } from '../hooks/useSystemConfig' export function LoginPage() { const { language } = useLanguage() @@ -21,6 +22,8 @@ export function LoginPage() { const [loading, setLoading] = useState(false) const [adminPassword, setAdminPassword] = useState('') const adminMode = false + const { config: systemConfig } = useSystemConfig() + const registrationEnabled = systemConfig?.registration_enabled !== false const handleAdminLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -313,7 +316,7 @@ export function LoginPage() { {/* Register Link */} - {!adminMode && ( + {!adminMode && registrationEnabled && (

还没有账户?{' '} diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 5f31c9d7..4131ad2c 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -9,6 +9,7 @@ import { copyWithToast } from '../lib/clipboard' import { Eye, EyeOff } from 'lucide-react' import { Input } from './ui/input' import PasswordChecklist from 'react-password-checklist' +import { RegistrationDisabled } from './RegistrationDisabled' export function RegisterPage() { const { language } = useLanguage() @@ -22,6 +23,7 @@ export function RegisterPage() { const [confirmPassword, setConfirmPassword] = useState('') const [betaCode, setBetaCode] = useState('') const [betaMode, setBetaMode] = useState(false) + const [registrationEnabled, setRegistrationEnabled] = useState(true) const [otpCode, setOtpCode] = useState('') const [userID, setUserID] = useState('') const [otpSecret, setOtpSecret] = useState('') @@ -33,16 +35,22 @@ export function RegisterPage() { const [showConfirmPassword, setShowConfirmPassword] = useState(false) useEffect(() => { - // 获取系统配置,检查是否开启内测模式 + // 获取系统配置,检查是否开启内测模式和注册功能 getSystemConfig() .then((config) => { setBetaMode(config.beta_mode || false) + setRegistrationEnabled(config.registration_enabled !== false) }) .catch((err) => { console.error('Failed to fetch system config:', err) }) }, []) + // 如果注册功能被禁用,显示注册已关闭页面 + if (!registrationEnabled) { + return + } + const handleRegister = async (e: React.FormEvent) => { e.preventDefault() setError('') diff --git a/web/src/components/RegistrationDisabled.test.tsx b/web/src/components/RegistrationDisabled.test.tsx new file mode 100644 index 00000000..beec1b83 --- /dev/null +++ b/web/src/components/RegistrationDisabled.test.tsx @@ -0,0 +1,103 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { RegistrationDisabled } from './RegistrationDisabled' +import { LanguageProvider } from '../contexts/LanguageContext' + +// Mock useLanguage hook +vi.mock('../contexts/LanguageContext', async () => { + const actual = await vi.importActual('../contexts/LanguageContext') + return { + ...actual, + useLanguage: () => ({ language: 'en' }), + } +}) + +/** + * RegistrationDisabled Component Tests + * + * Tests the component that displays when registration is disabled + * This is part of the registration toggle feature + */ +describe('RegistrationDisabled Component', () => { + const renderComponent = () => { + return render( + + + + ) + } + + describe('Rendering', () => { + it('should render the component without errors', () => { + const { container } = renderComponent() + expect(container).toBeTruthy() + }) + + it('should display the NoFx logo', () => { + renderComponent() + const logo = screen.getByAltText('NoFx Logo') + expect(logo).toBeTruthy() + expect(logo.getAttribute('src')).toBe('/icons/nofx.svg') + }) + + it('should display registration closed heading', () => { + renderComponent() + const heading = screen.getByText('Registration Closed') + expect(heading).toBeTruthy() + }) + + it('should display registration closed message', () => { + renderComponent() + const message = screen.getByText(/User registration is currently disabled/i) + expect(message).toBeTruthy() + }) + + it('should display back to login button', () => { + renderComponent() + const button = screen.getByRole('button', { name: /back to login/i }) + expect(button).toBeTruthy() + }) + }) + + describe('Navigation', () => { + it('should navigate to login page when button is clicked', () => { + const pushStateSpy = vi.spyOn(window.history, 'pushState') + const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent') + + renderComponent() + const button = screen.getByRole('button', { name: /back to login/i }) + + fireEvent.click(button) + + expect(pushStateSpy).toHaveBeenCalledWith({}, '', '/login') + expect(dispatchEventSpy).toHaveBeenCalled() + + pushStateSpy.mockRestore() + dispatchEventSpy.mockRestore() + }) + }) + + describe('Styling', () => { + it('should have correct background color', () => { + const { container } = renderComponent() + const mainDiv = container.firstChild as HTMLElement + // Browser converts hex to rgb + expect(mainDiv.style.background).toMatch(/rgb\(11,\s*14,\s*17\)|#0B0E11/i) + }) + + it('should have correct text color', () => { + const { container } = renderComponent() + const mainDiv = container.firstChild as HTMLElement + // Browser converts hex to rgb + expect(mainDiv.style.color).toMatch(/rgb\(234,\s*236,\s*239\)|#EAECEF/i) + }) + + it('should have centered layout', () => { + const { container } = renderComponent() + const mainDiv = container.firstChild as HTMLElement + expect(mainDiv.className).toContain('flex') + expect(mainDiv.className).toContain('items-center') + expect(mainDiv.className).toContain('justify-center') + }) + }) +}) diff --git a/web/src/components/RegistrationDisabled.tsx b/web/src/components/RegistrationDisabled.tsx new file mode 100644 index 00000000..048a9d62 --- /dev/null +++ b/web/src/components/RegistrationDisabled.tsx @@ -0,0 +1,39 @@ +import { useLanguage } from '../contexts/LanguageContext' +import { t } from '../i18n/translations' + +export function RegistrationDisabled() { + const { language } = useLanguage() + + const handleBackToLogin = () => { + window.history.pushState({}, '', '/login') + window.dispatchEvent(new PopStateEvent('popstate')) + } + + return ( +

+
+ NoFx Logo +

+ {t('registrationClosed', language)} +

+

+ {t('registrationClosedMessage', language)} +

+ +
+
+ ) +} diff --git a/web/src/components/landing/LoginModal.tsx b/web/src/components/landing/LoginModal.tsx index 44032c6e..3abaf1b8 100644 --- a/web/src/components/landing/LoginModal.tsx +++ b/web/src/components/landing/LoginModal.tsx @@ -1,6 +1,7 @@ import { motion } from 'framer-motion' import { X } from 'lucide-react' import { t, Language } from '../../i18n/translations' +import { useSystemConfig } from '../../hooks/useSystemConfig' interface LoginModalProps { onClose: () => void @@ -8,6 +9,9 @@ interface LoginModalProps { } export default function LoginModal({ onClose, language }: LoginModalProps) { + const { config: systemConfig } = useSystemConfig() + const registrationEnabled = systemConfig?.registration_enabled !== false + return ( {t('signIn', language)} - { - window.history.pushState({}, '', '/register') - window.dispatchEvent(new PopStateEvent('popstate')) - onClose() - }} - className="block w-full px-6 py-3 rounded-lg font-semibold text-center" - style={{ - background: 'var(--brand-dark-gray)', - color: 'var(--brand-light-gray)', - border: '1px solid rgba(240, 185, 11, 0.2)', - }} - whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }} - whileTap={{ scale: 0.95 }} - > - {t('registerNewAccount', language)} - + {registrationEnabled && ( + { + window.history.pushState({}, '', '/register') + window.dispatchEvent(new PopStateEvent('popstate')) + onClose() + }} + className="block w-full px-6 py-3 rounded-lg font-semibold text-center" + style={{ + background: 'var(--brand-dark-gray)', + color: 'var(--brand-light-gray)', + border: '1px solid rgba(240, 185, 11, 0.2)', + }} + whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }} + whileTap={{ scale: 0.95 }} + > + {t('registerNewAccount', language)} + + )}
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index c59164c9..9d17ace9 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -496,6 +496,9 @@ export const translations = { exitLogin: 'Sign Out', signIn: 'Sign In', signUp: 'Sign Up', + registrationClosed: 'Registration Closed', + registrationClosedMessage: + 'User registration is currently disabled. Please contact the administrator for access.', // Hero Section githubStarsInDays: '2.5K+ GitHub Stars in 3 days', @@ -1305,6 +1308,8 @@ export const translations = { exitLogin: '退出登录', signIn: '登录', signUp: '注册', + registrationClosed: '注册已关闭', + registrationClosedMessage: '平台当前不开放新用户注册,如需访问请联系管理员获取账号。', // Hero Section githubStarsInDays: '3 天内 2.5K+ GitHub Stars', diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts index 6be8f034..335aacd0 100644 --- a/web/src/lib/config.ts +++ b/web/src/lib/config.ts @@ -1,5 +1,6 @@ export interface SystemConfig { beta_mode: boolean + registration_enabled?: boolean } let configPromise: Promise | null = null diff --git a/web/src/lib/registrationToggle.test.ts b/web/src/lib/registrationToggle.test.ts new file mode 100644 index 00000000..9382fc72 --- /dev/null +++ b/web/src/lib/registrationToggle.test.ts @@ -0,0 +1,204 @@ +import { describe, it, expect } from 'vitest' + +/** + * Registration Toggle Feature Tests + * + * Tests the logic for determining whether registration is enabled + * This validates the registration_enabled configuration behavior + */ +describe('Registration Toggle Logic', () => { + describe('registration_enabled configuration', () => { + it('should default to true when registration_enabled is undefined', () => { + const config = {} + const registrationEnabled = (config as any).registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + + it('should be true when registration_enabled is explicitly true', () => { + const config = { registration_enabled: true } + const registrationEnabled = config.registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + + it('should be false when registration_enabled is explicitly false', () => { + const config = { registration_enabled: false } + const registrationEnabled = config.registration_enabled !== false + + expect(registrationEnabled).toBe(false) + }) + + it('should default to true when registration_enabled is null', () => { + const config = { registration_enabled: null } + const registrationEnabled = (config.registration_enabled as any) !== false + + expect(registrationEnabled).toBe(true) + }) + + it('should handle missing config gracefully', () => { + const config = null + const registrationEnabled = config?.registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + }) + + describe('UI component visibility logic', () => { + it('should show signup button when registration is enabled', () => { + const registrationEnabled = true + const shouldShowSignup = registrationEnabled + + expect(shouldShowSignup).toBe(true) + }) + + it('should hide signup button when registration is disabled', () => { + const registrationEnabled = false + const shouldShowSignup = registrationEnabled + + expect(shouldShowSignup).toBe(false) + }) + }) + + describe('conditional rendering patterns', () => { + it('should render signup link with registrationEnabled && pattern', () => { + const registrationEnabled = true + const signupElement = registrationEnabled && 'SignUpButton' + + expect(signupElement).toBe('SignUpButton') + }) + + it('should not render signup link when disabled', () => { + const registrationEnabled = false + const signupElement = registrationEnabled && 'SignUpButton' + + expect(signupElement).toBe(false) + }) + }) + + describe('SystemConfig interface compliance', () => { + interface SystemConfig { + beta_mode: boolean + registration_enabled?: boolean + } + + it('should have optional registration_enabled field', () => { + const config1: SystemConfig = { + beta_mode: false, + } + + const config2: SystemConfig = { + beta_mode: false, + registration_enabled: true, + } + + expect(config1.beta_mode).toBe(false) + expect(config2.registration_enabled).toBe(true) + }) + + it('should handle both beta_mode and registration_enabled', () => { + const config: SystemConfig = { + beta_mode: true, + registration_enabled: false, + } + + expect(config.beta_mode).toBe(true) + expect(config.registration_enabled).toBe(false) + }) + }) + + describe('edge cases', () => { + it('should treat empty string as truthy (not false)', () => { + const config = { registration_enabled: '' as any } + const registrationEnabled = config.registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + + it('should treat 0 as truthy (not false)', () => { + const config = { registration_enabled: 0 as any } + const registrationEnabled = config.registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + + it('should treat "false" string as truthy (not false)', () => { + const config = { registration_enabled: 'false' as any } + const registrationEnabled = config.registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + + it('should only treat boolean false as disabled', () => { + const testCases = [ + { value: false, expected: false }, + { value: true, expected: true }, + { value: null, expected: true }, + { value: undefined, expected: true }, + { value: 0, expected: true }, + { value: '', expected: true }, + { value: 'false', expected: true }, + { value: [], expected: true }, + { value: {}, expected: true }, + ] + + testCases.forEach(({ value, expected }) => { + const config = { registration_enabled: value as any } + const registrationEnabled = config.registration_enabled !== false + expect(registrationEnabled).toBe(expected) + }) + }) + }) + + describe('backend API response handling', () => { + it('should parse backend response with registration_enabled', () => { + const apiResponse = { + beta_mode: false, + default_coins: ['BTCUSDT'], + btc_eth_leverage: 5, + altcoin_leverage: 5, + registration_enabled: true, + } + + expect(apiResponse.registration_enabled).toBe(true) + }) + + it('should handle backend response without registration_enabled', () => { + const apiResponse = { + beta_mode: false, + default_coins: ['BTCUSDT'], + btc_eth_leverage: 5, + altcoin_leverage: 5, + } + + const registrationEnabled = + (apiResponse as any).registration_enabled !== false + + expect(registrationEnabled).toBe(true) + }) + }) + + describe('multi-location consistency', () => { + const systemConfig = { registration_enabled: false } + + it('should have consistent behavior across LoginPage', () => { + const registrationEnabled = systemConfig?.registration_enabled !== false + expect(registrationEnabled).toBe(false) + }) + + it('should have consistent behavior across RegisterPage', () => { + const registrationEnabled = systemConfig?.registration_enabled !== false + expect(registrationEnabled).toBe(false) + }) + + it('should have consistent behavior across HeaderBar', () => { + const registrationEnabled = systemConfig?.registration_enabled !== false + expect(registrationEnabled).toBe(false) + }) + + it('should have consistent behavior across LoginModal', () => { + const registrationEnabled = systemConfig?.registration_enabled !== false + expect(registrationEnabled).toBe(false) + }) + }) +})