feat(web-console): support username and password authentication via env vars#93
feat(web-console): support username and password authentication via env vars#93
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enhances the web console authentication system by adding support for username and password authentication via environment variables (WEB_CONSOLE_USERNAME and WEB_CONSOLE_PASSWORD). The implementation maintains backward compatibility with the existing API_KEYS authentication method and provides a dynamic login form that adapts based on the server configuration.
Changes:
- Added username/password authentication via environment variables with three modes: API key only, password only, or username+password
- Implemented
/api/auth/configendpoint to enable dynamic form rendering on the frontend - Added internationalization strings for new UI elements (username/password placeholders and error messages)
- Updated documentation in README files and .env.example to describe the new authentication options
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/locales/zh.json | Added Chinese translations for new authentication UI elements (login heading, credentials error, username/password placeholders) |
| ui/locales/en.json | Added English translations for new authentication UI elements (login heading, credentials error, username/password placeholders) |
| ui/app/pages/LoginPage.vue | Modified login form to dynamically render username/password fields based on server config fetched from /api/auth/config endpoint |
| src/routes/AuthRoutes.js | Added /api/auth/config endpoint and implemented multi-mode authentication logic supporting username+password, password-only, or API key modes |
| README_EN.md | Documented new WEB_CONSOLE_USERNAME and WEB_CONSOLE_PASSWORD environment variables in configuration table |
| README.md | Documented new WEB_CONSOLE_USERNAME and WEB_CONSOLE_PASSWORD environment variables in configuration table (Chinese) |
| .env.example | Added example configuration entries for WEB_CONSOLE_USERNAME and WEB_CONSOLE_PASSWORD with usage comments |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (expectedUsername && expectedPassword) { | ||
| if (username === expectedUsername && submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else if (!expectedUsername && expectedPassword) { | ||
| if (submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } |
There was a problem hiding this comment.
The authentication logic uses direct string comparison for username and password validation, which is vulnerable to timing attacks. An attacker could potentially use timing differences to determine valid usernames or passwords character by character. Consider using a constant-time comparison function for security-sensitive string comparisons.
| const submittedPassword = password || apiKey; | ||
| const expectedUsername = process.env.WEB_CONSOLE_USERNAME; | ||
| const expectedPassword = process.env.WEB_CONSOLE_PASSWORD; | ||
|
|
||
| if (expectedUsername && expectedPassword) { | ||
| if (username === expectedUsername && submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else if (!expectedUsername && expectedPassword) { | ||
| if (submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else { | ||
| if (submittedPassword && this.config.apiKeys.includes(submittedPassword)) { |
There was a problem hiding this comment.
When only the password is set (without username), the authentication logic falls back to accepting the password in the 'password' field OR 'apiKey' field (via the submittedPassword variable on line 152). This creates an ambiguous authentication flow where the same credential can be submitted via different field names. Consider enforcing that only the appropriate field name is used for each authentication mode to prevent confusion and potential security issues.
| const submittedPassword = password || apiKey; | |
| const expectedUsername = process.env.WEB_CONSOLE_USERNAME; | |
| const expectedPassword = process.env.WEB_CONSOLE_PASSWORD; | |
| if (expectedUsername && expectedPassword) { | |
| if (username === expectedUsername && submittedPassword === expectedPassword) { | |
| authSuccess = true; | |
| } | |
| } else if (!expectedUsername && expectedPassword) { | |
| if (submittedPassword === expectedPassword) { | |
| authSuccess = true; | |
| } | |
| } else { | |
| if (submittedPassword && this.config.apiKeys.includes(submittedPassword)) { | |
| const expectedUsername = process.env.WEB_CONSOLE_USERNAME; | |
| const expectedPassword = process.env.WEB_CONSOLE_PASSWORD; | |
| // Username + password mode: require credentials in username/password fields | |
| if (expectedUsername && expectedPassword) { | |
| if (username === expectedUsername && password === expectedPassword) { | |
| authSuccess = true; | |
| } | |
| // Password-only mode: require shared secret in password field | |
| } else if (!expectedUsername && expectedPassword) { | |
| if (password === expectedPassword) { | |
| authSuccess = true; | |
| } | |
| // API key mode: require API key in apiKey field | |
| } else { | |
| if (apiKey && this.config.apiKeys.includes(apiKey)) { |
| WEB_CONSOLE_USERNAME= | ||
|
|
||
| # Web console password (optional) | ||
| # If only password is set, the login page will only ask for the password. | ||
| # If neither is set, the system falls back to API_KEYS for console login. |
There was a problem hiding this comment.
The documentation states that WEB_CONSOLE_USERNAME is used "with password" but doesn't clarify what happens if WEB_CONSOLE_USERNAME is set without WEB_CONSOLE_PASSWORD. Based on the backend logic, setting only WEB_CONSOLE_USERNAME has no effect - the system will fall back to API_KEYS. Consider adding explicit documentation stating that WEB_CONSOLE_USERNAME requires WEB_CONSOLE_PASSWORD to be set, or that WEB_CONSOLE_USERNAME alone is ignored.
| WEB_CONSOLE_USERNAME= | |
| # Web console password (optional) | |
| # If only password is set, the login page will only ask for the password. | |
| # If neither is set, the system falls back to API_KEYS for console login. | |
| # If only username is set (without password), it is ignored and the system | |
| # falls back to API_KEYS for console login. | |
| WEB_CONSOLE_USERNAME= | |
| # Web console password (optional) | |
| # If only password is set, the login page will only ask for the password. | |
| # If neither is set (or only username is set), the system falls back to API_KEYS | |
| # for console login. |
| const { apiKey, username, password } = req.body; | ||
| let authSuccess = false; | ||
| const submittedPassword = password || apiKey; | ||
| const expectedUsername = process.env.WEB_CONSOLE_USERNAME; | ||
| const expectedPassword = process.env.WEB_CONSOLE_PASSWORD; | ||
|
|
||
| if (expectedUsername && expectedPassword) { | ||
| if (username === expectedUsername && submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else if (!expectedUsername && expectedPassword) { | ||
| if (submittedPassword === expectedPassword) { | ||
| authSuccess = true; |
There was a problem hiding this comment.
The username and password values from environment variables and request body are not trimmed before comparison. This means that leading or trailing whitespace in credentials will cause authentication to fail. While environment variables typically don't have leading/trailing whitespace, HTML form inputs can include it if users accidentally add spaces. Consider trimming the input values before comparison, or document that exact string matching (including whitespace) is required.
| const expectedUsername = process.env.WEB_CONSOLE_USERNAME; | ||
| const expectedPassword = process.env.WEB_CONSOLE_PASSWORD; | ||
|
|
||
| if (expectedUsername && expectedPassword) { | ||
| if (username === expectedUsername && submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else if (!expectedUsername && expectedPassword) { | ||
| if (submittedPassword === expectedPassword) { | ||
| authSuccess = true; |
There was a problem hiding this comment.
The password is stored in plain text in environment variables and compared directly without hashing. While environment variables are a common practice for credentials, the authentication system should ideally use hashed passwords with a salt for better security. Consider implementing password hashing (e.g., bcrypt, scrypt, or argon2) to protect credentials even if environment variables are exposed.
| try { | ||
| const res = await fetch("/api/auth/config"); | ||
| if (res.ok) { | ||
| const data = await res.json(); | ||
| requireUsername.value = data.requireUsername; | ||
| requirePassword.value = data.requirePassword; | ||
| } | ||
| } catch (err) { | ||
| console.error("Failed to load auth config", err); | ||
| } finally { | ||
| configLoaded.value = true; | ||
| } |
There was a problem hiding this comment.
The auth config fetch failure is only logged to the console but doesn't provide any user-visible feedback. If the config endpoint fails to load, the user will see a blank login form (since v-if="configLoaded" will eventually become true but with default false values for requireUsername and requirePassword). Consider displaying an error message to the user when the config fails to load, or implementing a retry mechanism.
| if (expectedUsername && expectedPassword) { | ||
| if (username === expectedUsername && submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else if (!expectedUsername && expectedPassword) { | ||
| if (submittedPassword === expectedPassword) { | ||
| authSuccess = true; | ||
| } | ||
| } else { | ||
| if (submittedPassword && this.config.apiKeys.includes(submittedPassword)) { | ||
| authSuccess = true; | ||
| } | ||
| } |
There was a problem hiding this comment.
The authentication logic doesn't handle the edge case where WEB_CONSOLE_USERNAME is set but WEB_CONSOLE_PASSWORD is not set. In this scenario, the code will fall through to the API_KEYS check (line 165) without any username validation. This could lead to unexpected behavior where a username field appears in the UI but is ignored during authentication. Consider adding validation to ensure that if WEB_CONSOLE_USERNAME is set, WEB_CONSOLE_PASSWORD must also be set, or handle this case explicitly.
| const { apiKey, username, password } = req.body; | ||
| let authSuccess = false; | ||
| const submittedPassword = password || apiKey; | ||
| const expectedUsername = process.env.WEB_CONSOLE_USERNAME; | ||
| const expectedPassword = process.env.WEB_CONSOLE_PASSWORD; |
There was a problem hiding this comment.
There's no input validation or sanitization on the username and password fields received from the request body. While these are only compared against environment variables (not used in database queries or command execution), it's a best practice to validate input length and potentially log/block suspicious inputs. Consider adding basic validation such as maximum length limits to prevent potential DoS attacks through extremely long input strings.
| </svg> | ||
| </button> | ||
| <div class="login-content"> | ||
| <div v-if="configLoaded" class="login-content"> |
There was a problem hiding this comment.
While the config is loading, the login form content is hidden with v-if="configLoaded". This provides a good UX to prevent form submission before config loads. However, there's no loading indicator shown to the user during this time. Consider adding a loading spinner or message to indicate that the page is initializing, especially on slower network connections where the fetch might take noticeable time.
简介
此 PR 增强了网页控制台的登录功能,新增了支持通过只读环境变量实现用户名和密码登录的机制。
变更内容
.env中新增了WEB_CONSOLE_USERNAME和WEB_CONSOLE_PASSWORD的支持。/api/auth/config接口,使前端能依据服务端配置动态渲染输入框(仅密码、用户名+密码、仅 API 密钥)。API_KEYS逻辑进行登录。README和.env.example。