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)
This commit is contained in:
Lawrence Liu
2025-11-13 01:37:24 +08:00
committed by tangmengqiu
parent f8d1cbb670
commit e497898ef1
13 changed files with 437 additions and 750 deletions

View File

@@ -197,11 +197,18 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) {
betaModeStr, _ := s.database.GetSystemConfig("beta_mode") betaModeStr, _ := s.database.GetSystemConfig("beta_mode")
betaMode := betaModeStr == "true" 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{ c.JSON(http.StatusOK, gin.H{
"beta_mode": betaMode, "beta_mode": betaMode,
"default_coins": defaultCoins, "default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage, "btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage, "altcoin_leverage": altcoinLeverage,
"registration_enabled": registrationEnabled,
}) })
} }
@@ -1704,6 +1711,14 @@ func (s *Server) handleLogout(c *gin.Context) {
// handleRegister 处理用户注册请求 // handleRegister 处理用户注册请求
func (s *Server) handleRegister(c *gin.Context) { 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 { var req struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email"`

View File

@@ -1,5 +1,6 @@
{ {
"beta_mode": false, "beta_mode": false,
"registration_enabled": true,
"leverage": { "leverage": {
"btc_eth_leverage": 5, "btc_eth_leverage": 5,
"altcoin_leverage": 5 "altcoin_leverage": 5

View File

@@ -324,6 +324,7 @@ func (d *Database) initDefaultData() error {
"btc_eth_leverage": "5", // BTC/ETH杠杆倍数 "btc_eth_leverage": "5", // BTC/ETH杠杆倍数
"altcoin_leverage": "5", // 山寨币杠杆倍数 "altcoin_leverage": "5", // 山寨币杠杆倍数
"jwt_secret": "", // JWT密钥默认为空由config.json或系统生成 "jwt_secret": "", // JWT密钥默认为空由config.json或系统生成
"registration_enabled": "true", // 默认允许注册
} }
for key, value := range systemConfigs { for key, value := range systemConfigs {

706
web/package-lock.json generated
View File

@@ -1160,192 +1160,6 @@
"url": "https://github.com/sponsors/nzakas" "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": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1408,26 +1222,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1463,37 +1257,6 @@
"node": ">= 8" "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": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -2406,15 +2169,6 @@
"@types/react": "^18.0.0" "@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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.3", "version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", "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" "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": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -5055,18 +4689,6 @@
"node": ">=6.9.0" "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": { "node_modules/get-east-asian-width": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
@@ -5228,18 +4850,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/has-bigints": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -5333,15 +4943,6 @@
"node": ">= 0.4" "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": { "node_modules/hermes-estree": {
"version": "0.25.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -5737,15 +5338,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -6515,104 +6107,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true "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": { "node_modules/mz": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -6842,15 +6336,6 @@
"node": ">= 0.8.0" "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": { "node_modules/own-keys": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -6980,15 +6465,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true "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": { "node_modules/pathe": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -7645,18 +7121,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -7704,15 +7168,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/reusify": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -8123,18 +7578,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/std-env": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
@@ -8156,15 +7599,6 @@
"node": ">= 0.4" "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": { "node_modules/string-argv": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
@@ -8735,21 +8169,6 @@
"node": ">= 0.8.0" "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": { "node_modules/typed-array-buffer": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "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" "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": { "node_modules/update-browserslist-db": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
@@ -10510,18 +9917,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -10541,92 +9936,6 @@
"node": ">= 14.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": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -10640,21 +9949,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zod": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",

View File

@@ -4,6 +4,7 @@ import { motion } from 'framer-motion'
import { Menu, X, ChevronDown } from 'lucide-react' import { Menu, X, ChevronDown } from 'lucide-react'
import { t, type Language } from '../i18n/translations' import { t, type Language } from '../i18n/translations'
import { Container } from './Container' import { Container } from './Container'
import { useSystemConfig } from '../hooks/useSystemConfig'
interface HeaderBarProps { interface HeaderBarProps {
onLoginClick?: () => void onLoginClick?: () => void
@@ -33,6 +34,8 @@ export default function HeaderBar({
const [userDropdownOpen, setUserDropdownOpen] = useState(false) const [userDropdownOpen, setUserDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
const userDropdownRef = useRef<HTMLDivElement>(null) const userDropdownRef = useRef<HTMLDivElement>(null)
const { config: systemConfig } = useSystemConfig()
const registrationEnabled = systemConfig?.registration_enabled !== false
// Close dropdown when clicking outside // Close dropdown when clicking outside
useEffect(() => { useEffect(() => {
@@ -464,16 +467,18 @@ export default function HeaderBar({
> >
{t('signIn', language)} {t('signIn', language)}
</a> </a>
<a {registrationEnabled && (
href="/register" <a
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90" href="/register"
style={{ className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90"
background: 'var(--brand-yellow)', style={{
color: 'var(--brand-black)', background: 'var(--brand-yellow)',
}} color: 'var(--brand-black)',
> }}
{t('signUp', language)} >
</a> {t('signUp', language)}
</a>
)}
</div> </div>
) )
)} )}
@@ -901,17 +906,19 @@ export default function HeaderBar({
> >
{t('signIn', language)} {t('signIn', language)}
</a> </a>
<a {registrationEnabled && (
href="/register" <a
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors" href="/register"
style={{ className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors"
background: 'var(--brand-yellow)', style={{
color: 'var(--brand-black)', background: 'var(--brand-yellow)',
}} color: 'var(--brand-black)',
onClick={() => setMobileMenuOpen(false)} }}
> onClick={() => setMobileMenuOpen(false)}
{t('signUp', language)} >
</a> {t('signUp', language)}
</a>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -6,6 +6,7 @@ import { t } from '../i18n/translations'
import { Eye, EyeOff } from 'lucide-react' import { Eye, EyeOff } from 'lucide-react'
import { Input } from './ui/input' import { Input } from './ui/input'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useSystemConfig } from '../hooks/useSystemConfig'
export function LoginPage() { export function LoginPage() {
const { language } = useLanguage() const { language } = useLanguage()
@@ -21,6 +22,8 @@ export function LoginPage() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [adminPassword, setAdminPassword] = useState('') const [adminPassword, setAdminPassword] = useState('')
const adminMode = false const adminMode = false
const { config: systemConfig } = useSystemConfig()
const registrationEnabled = systemConfig?.registration_enabled !== false
const handleAdminLogin = async (e: React.FormEvent) => { const handleAdminLogin = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@@ -313,7 +316,7 @@ export function LoginPage() {
</div> </div>
{/* Register Link */} {/* Register Link */}
{!adminMode && ( {!adminMode && registrationEnabled && (
<div className="text-center mt-6"> <div className="text-center mt-6">
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}> <p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{' '} {' '}

View File

@@ -9,6 +9,7 @@ import { copyWithToast } from '../lib/clipboard'
import { Eye, EyeOff } from 'lucide-react' import { Eye, EyeOff } from 'lucide-react'
import { Input } from './ui/input' import { Input } from './ui/input'
import PasswordChecklist from 'react-password-checklist' import PasswordChecklist from 'react-password-checklist'
import { RegistrationDisabled } from './RegistrationDisabled'
export function RegisterPage() { export function RegisterPage() {
const { language } = useLanguage() const { language } = useLanguage()
@@ -22,6 +23,7 @@ export function RegisterPage() {
const [confirmPassword, setConfirmPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('')
const [betaCode, setBetaCode] = useState('') const [betaCode, setBetaCode] = useState('')
const [betaMode, setBetaMode] = useState(false) const [betaMode, setBetaMode] = useState(false)
const [registrationEnabled, setRegistrationEnabled] = useState(true)
const [otpCode, setOtpCode] = useState('') const [otpCode, setOtpCode] = useState('')
const [userID, setUserID] = useState('') const [userID, setUserID] = useState('')
const [otpSecret, setOtpSecret] = useState('') const [otpSecret, setOtpSecret] = useState('')
@@ -33,16 +35,22 @@ export function RegisterPage() {
const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false)
useEffect(() => { useEffect(() => {
// 获取系统配置,检查是否开启内测模式 // 获取系统配置,检查是否开启内测模式和注册功能
getSystemConfig() getSystemConfig()
.then((config) => { .then((config) => {
setBetaMode(config.beta_mode || false) setBetaMode(config.beta_mode || false)
setRegistrationEnabled(config.registration_enabled !== false)
}) })
.catch((err) => { .catch((err) => {
console.error('Failed to fetch system config:', err) console.error('Failed to fetch system config:', err)
}) })
}, []) }, [])
// 如果注册功能被禁用,显示注册已关闭页面
if (!registrationEnabled) {
return <RegistrationDisabled />
}
const handleRegister = async (e: React.FormEvent) => { const handleRegister = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError('') setError('')

View File

@@ -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(
<LanguageProvider>
<RegistrationDisabled />
</LanguageProvider>
)
}
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')
})
})
})

View File

@@ -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 (
<div
className="min-h-screen flex items-center justify-center"
style={{ background: '#0B0E11', color: '#EAECEF' }}
>
<div className="text-center max-w-md px-6">
<img
src="/icons/nofx.svg"
alt="NoFx Logo"
className="w-16 h-16 mx-auto mb-4"
/>
<h1 className="text-2xl font-semibold mb-3">
{t('registrationClosed', language)}
</h1>
<p className="text-sm text-gray-400">
{t('registrationClosedMessage', language)}
</p>
<button
className="mt-6 px-4 py-2 rounded text-sm font-semibold transition-colors hover:opacity-90"
style={{ background: '#F0B90B', color: '#000' }}
onClick={handleBackToLogin}
>
{t('backToLogin', language)}
</button>
</div>
</div>
)
}

View File

@@ -1,6 +1,7 @@
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { X } from 'lucide-react' import { X } from 'lucide-react'
import { t, Language } from '../../i18n/translations' import { t, Language } from '../../i18n/translations'
import { useSystemConfig } from '../../hooks/useSystemConfig'
interface LoginModalProps { interface LoginModalProps {
onClose: () => void onClose: () => void
@@ -8,6 +9,9 @@ interface LoginModalProps {
} }
export default function LoginModal({ onClose, language }: LoginModalProps) { export default function LoginModal({ onClose, language }: LoginModalProps) {
const { config: systemConfig } = useSystemConfig()
const registrationEnabled = systemConfig?.registration_enabled !== false
return ( return (
<motion.div <motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-4" className="fixed inset-0 z-50 flex items-center justify-center p-4"
@@ -66,23 +70,25 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
> >
{t('signIn', language)} {t('signIn', language)}
</motion.button> </motion.button>
<motion.button {registrationEnabled && (
onClick={() => { <motion.button
window.history.pushState({}, '', '/register') onClick={() => {
window.dispatchEvent(new PopStateEvent('popstate')) window.history.pushState({}, '', '/register')
onClose() window.dispatchEvent(new PopStateEvent('popstate'))
}} onClose()
className="block w-full px-6 py-3 rounded-lg font-semibold text-center" }}
style={{ className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
background: 'var(--brand-dark-gray)', style={{
color: 'var(--brand-light-gray)', background: 'var(--brand-dark-gray)',
border: '1px solid rgba(240, 185, 11, 0.2)', 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 }} whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }}
> whileTap={{ scale: 0.95 }}
{t('registerNewAccount', language)} >
</motion.button> {t('registerNewAccount', language)}
</motion.button>
)}
</div> </div>
</motion.div> </motion.div>
</motion.div> </motion.div>

View File

@@ -496,6 +496,9 @@ export const translations = {
exitLogin: 'Sign Out', exitLogin: 'Sign Out',
signIn: 'Sign In', signIn: 'Sign In',
signUp: 'Sign Up', signUp: 'Sign Up',
registrationClosed: 'Registration Closed',
registrationClosedMessage:
'User registration is currently disabled. Please contact the administrator for access.',
// Hero Section // Hero Section
githubStarsInDays: '2.5K+ GitHub Stars in 3 days', githubStarsInDays: '2.5K+ GitHub Stars in 3 days',
@@ -1305,6 +1308,8 @@ export const translations = {
exitLogin: '退出登录', exitLogin: '退出登录',
signIn: '登录', signIn: '登录',
signUp: '注册', signUp: '注册',
registrationClosed: '注册已关闭',
registrationClosedMessage: '平台当前不开放新用户注册,如需访问请联系管理员获取账号。',
// Hero Section // Hero Section
githubStarsInDays: '3 天内 2.5K+ GitHub Stars', githubStarsInDays: '3 天内 2.5K+ GitHub Stars',

View File

@@ -1,5 +1,6 @@
export interface SystemConfig { export interface SystemConfig {
beta_mode: boolean beta_mode: boolean
registration_enabled?: boolean
} }
let configPromise: Promise<SystemConfig> | null = null let configPromise: Promise<SystemConfig> | null = null

View File

@@ -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)
})
})
})