fix(install): correct Redis URL auth for 1Panel root user

Password must use redis://:pass@ or redis://root:pass@ host; wizard adds
redis_user field defaulting to root on 1Panel Docker installs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Deploy
2026-06-06 06:59:45 +08:00
parent da061d35db
commit 807f4c2448
5 changed files with 88 additions and 15 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ Docker 栈**仅含 Nexus 容器**。MySQL / Redis 在 **1Panel 应用商店**安
1. **应用商店** → 安装 **MySQL**、**Redis**
2. **数据库** → 创建库 `nexus`、用户 `nexus`(仅授权 `nexus` 库),记下密码
3. `nx update` 后打开安装向导步骤 3(主机应已预填 `1Panel-mysql-…`
4. 填写 MySQL 密码;Redis 若设了密码一并填写
4. 填写 MySQL 密码;Redis**用户名 `root`** + 应用参数中的密码(见 1Panel 文档)
### 常见错误 1045Access denied for 'nexus'@'172.18.x.x'
@@ -0,0 +1,33 @@
# 2026-06-06 — 修复安装向导 Redis URL 与 1Panel root 用户
## 摘要
步骤 3 Redis 连接失败根因:`_build_redis_url` 生成 `redis://pass@host`(密码被当成用户名);1Panel Redis ACL 用户名为 `root`
## 修复
- URL 密码格式改为 `redis://:pass@host``redis://root:pass@host`
- 向导新增 Redis 用户名,Docker/1Panel 预填 `root`
- 错误提示说明 1Panel 用法
## 涉及文件
- `server/api/install.py`
- `web/app/install.html`
- `tests/test_security_unit.py`
- `deploy/README-1panel.md`
## 验证
```bash
pytest tests/test_security_unit.py -k build_redis_url -q
bash deploy/sync-install-wizard-to-container.sh
```
步骤 3Redis 用户 `root`、密码 `redis_WP3NyN`、主机 `1Panel-redis-xxxx` → 检测通过。
## 已有 .env 时手动修正
```bash
NEXUS_REDIS_URL="redis://root:你的密码@1Panel-redis-xxxx:6379/0"
```
+16 -8
View File
@@ -62,6 +62,7 @@ class InitDbRequest(BaseModel):
redis_host: str = "127.0.0.1"
redis_port: str = "6379"
redis_db: str = "0"
redis_user: str = ""
redis_pass: str = ""
api_port: str = "8600"
timezone: str = "Asia/Shanghai"
@@ -92,11 +93,17 @@ def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str:
def _build_redis_url(req: InitDbRequest) -> str:
url = "redis://"
if req.redis_pass:
url += f"{quote_plus(req.redis_pass)}@"
url += f"{req.redis_host}:{req.redis_port}/{req.redis_db}"
return url
"""Build redis:// URL. Password-only auth must use redis://:pass@host (leading colon)."""
user = (req.redis_user or "").strip()
password = (req.redis_pass or "").strip()
auth = ""
if user and password:
auth = f"{quote_plus(user)}:{quote_plus(password)}@"
elif password:
auth = f":{quote_plus(password)}@"
elif user:
auth = f"{quote_plus(user)}@"
return f"redis://{auth}{req.redis_host}:{req.redis_port}/{req.redis_db}"
def _read_1panel_hosts_file() -> dict[str, str]:
@@ -236,10 +243,10 @@ def _mysql_connect_error_detail(host: str, port: int) -> str:
def _redis_auth_error_detail() -> str:
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
return (
"Redis 密码错误或用户被禁用(1Panel Redis 请在步骤 3 填写应用参数中的密码;"
"无密码则留空)。主机为 1Panel-redis-xxxx不是 127.0.0.1。"
"Redis 认证失败。1Panel Redis:用户名填 root,密码填应用参数中的随机密码;"
"主机为 1Panel-redis-xxxx不是 127.0.0.1)。无 ACL 的旧版 Redis 用户名可留空"
)
return "Redis 密码错误或需要 AUTH;请核对 Redis 密码(无密码则留空)。"
return "Redis 密码错误或需要 AUTH;请核对用户名/密码(无密码则留空)。"
def _redis_connect_error_detail(host: str, port: int) -> str:
@@ -344,6 +351,7 @@ def _docker_wizard_defaults() -> dict[str, str] | None:
"redis_host": redis_host,
"redis_port": "6379",
"redis_db": "0",
"redis_user": "root",
}
+23
View File
@@ -211,6 +211,29 @@ async def test_install_test_credentials_all_pass(monkeypatch, tmp_path):
assert len(out["checks"]) == 2
def test_build_redis_url_password_with_leading_colon():
from server.api.install import InitDbRequest, _build_redis_url
req = InitDbRequest(
db_pass="db",
redis_host="1Panel-redis-x",
redis_pass="redis_WP3NyN",
)
assert _build_redis_url(req) == "redis://:redis_WP3NyN@1Panel-redis-x:6379/0"
def test_build_redis_url_1panel_root_user():
from server.api.install import InitDbRequest, _build_redis_url
req = InitDbRequest(
db_pass="db",
redis_host="1Panel-redis-x",
redis_user="root",
redis_pass="redis_WP3NyN",
)
assert _build_redis_url(req) == "redis://root:redis_WP3NyN@1Panel-redis-x:6379/0"
def test_install_reject_repeat_init_when_env_exists(monkeypatch, tmp_path):
from fastapi import HTTPException
from server.api import install as install_api
+15 -6
View File
@@ -197,14 +197,21 @@
</div>
<div class="grid grid-cols-2 gap-4 mt-3">
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
<input x-model="form.redis_db" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码(可选)</label>
<input x-model="form.redis_pass" @input="invalidateCredCheck()" type="password" placeholder="无密码留空"
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 用户名</label>
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel Redis 填 <code class="bg-slate-100 px-1 rounded">root</code>;无用户留空</p>
<input x-model="form.redis_user" @input="invalidateCredCheck()" placeholder="1Panel: root"
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
<div>
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 数据库号</label>
<input x-model="form.redis_db" @input="invalidateCredCheck()" class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
<div class="mt-3">
<label class="block font-semibold text-sm text-slate-700 mb-1">Redis 密码</label>
<p class="text-xs text-[var(--text-secondary)] mb-1">1Panel → Redis → 参数中的密码;无密码留空</p>
<input x-model="form.redis_pass" @input="invalidateCredCheck()" type="password" placeholder="无密码留空"
class="w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:border-blue-500 focus:outline-none transition">
</div>
</div>
@@ -636,6 +643,7 @@ function installWizard() {
redis_host: '127.0.0.1',
redis_port: '6379',
redis_db: '0',
redis_user: '',
redis_pass: '',
api_port: '8600',
timezone: 'Asia/Shanghai',
@@ -674,6 +682,7 @@ function installWizard() {
if (defaults.db_port) this.form.db_port = defaults.db_port;
if (defaults.redis_port) this.form.redis_port = defaults.redis_port;
if (defaults.redis_db) this.form.redis_db = defaults.redis_db;
if (defaults.redis_user && !this.form.redis_user) this.form.redis_user = defaults.redis_user;
},
async refreshDockerDefaults() {