feat: add administrator password recovery menu
This commit is contained in:
@@ -83,6 +83,8 @@ sudo newszxcn-email rollback
|
|||||||
```bash
|
```bash
|
||||||
sudo ns
|
sudo ns
|
||||||
sudo newszxcn-email guide
|
sudo newszxcn-email guide
|
||||||
|
sudo newszxcn-email credentials
|
||||||
|
sudo newszxcn-email reset-password
|
||||||
sudo newszxcn-email status
|
sudo newszxcn-email status
|
||||||
sudo newszxcn-email logs
|
sudo newszxcn-email logs
|
||||||
sudo newszxcn-email restart
|
sudo newszxcn-email restart
|
||||||
@@ -90,6 +92,8 @@ sudo newszxcn-email certificate
|
|||||||
sudo newszxcn-email uninstall
|
sudo newszxcn-email uninstall
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`credentials` 显示安装或最近一次命令行重置时记录的管理员登录信息。数据库中的密码采用 bcrypt 哈希,无法反向查看;如果管理员后来在网页修改过密码,记录值可能已经失效。`reset-password` 只重置配置管理员的统一登录密码,并同步该管理员名下邮箱的 SMTP/IMAP 密码,不会修改普通用户或其邮箱。重置前会先备份并校验数据库。
|
||||||
|
|
||||||
`uninstall` 会移除容器和自动生成的 Nginx 配置,但不删除 `/opt/newszxcn-email` 中的配置、证书、数据库与邮件。卸载时可以单独停止当前域名的 acme.sh 自动续期,不会影响 acme.sh 管理的其他域名。
|
`uninstall` 会移除容器和自动生成的 Nginx 配置,但不删除 `/opt/newszxcn-email` 中的配置、证书、数据库与邮件。卸载时可以单独停止当前域名的 acme.sh 自动续期,不会影响 acme.sh 管理的其他域名。
|
||||||
|
|
||||||
## DNS 与端口
|
## DNS 与端口
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ sudo newszxcn-email status
|
|||||||
sudo newszxcn-email logs
|
sudo newszxcn-email logs
|
||||||
sudo newszxcn-email rollback
|
sudo newszxcn-email rollback
|
||||||
sudo newszxcn-email guide
|
sudo newszxcn-email guide
|
||||||
|
sudo newszxcn-email credentials
|
||||||
|
sudo newszxcn-email reset-password
|
||||||
```
|
```
|
||||||
|
|
||||||
一键安装会把配置和数据放在 `/opt/newszxcn-email`,并部署内部 Watchtower 更新服务。该服务不映射公网端口,仅接受带随机令牌的容器内请求;后台“立即更新”也只允许超级管理员执行。
|
一键安装会把配置和数据放在 `/opt/newszxcn-email`,并部署内部 Watchtower 更新服务。该服务不映射公网端口,仅接受带随机令牌的容器内请求;后台“立即更新”也只允许超级管理员执行。
|
||||||
@@ -24,6 +26,8 @@ sudo newszxcn-email guide
|
|||||||
|
|
||||||
安装后输入 `ns` 可以打开统一管理菜单。更新前会创建包含数据库、镜像、Compose、环境、安装脚本和 Nginx 的回滚快照;更新或健康检查失败时会自动恢复。手动完整回滚前还会单独备份当前数据库,回滚镜像会保持锁定到下一次更新。
|
安装后输入 `ns` 可以打开统一管理菜单。更新前会创建包含数据库、镜像、Compose、环境、安装脚本和 Nginx 的回滚快照;更新或健康检查失败时会自动恢复。手动完整回滚前还会单独备份当前数据库,回滚镜像会保持锁定到下一次更新。
|
||||||
|
|
||||||
|
菜单可查看安装或最近一次命令行重置时记录的管理员登录信息,也可单独重置配置管理员的统一登录密码。密码采用 bcrypt 哈希,无法从数据库反向解密;网页修改密码后,脚本中的记录可能已经失效。命令行重置前会备份并校验数据库,同时同步该管理员名下邮箱的 SMTP/IMAP 密码,不会修改普通用户或其邮箱。
|
||||||
|
|
||||||
## 最简单部署:单容器镜像版
|
## 最简单部署:单容器镜像版
|
||||||
|
|
||||||
服务器上不需要源码构建,只要 `docker-compose.yml` 和 `.env` 即可。
|
服务器上不需要源码构建,只要 `docker-compose.yml` 和 `.env` 即可。
|
||||||
|
|||||||
@@ -103,12 +103,16 @@ sudo newszxcn-email logs
|
|||||||
sudo newszxcn-email certificate
|
sudo newszxcn-email certificate
|
||||||
sudo newszxcn-email rollback
|
sudo newszxcn-email rollback
|
||||||
sudo newszxcn-email guide
|
sudo newszxcn-email guide
|
||||||
|
sudo newszxcn-email credentials
|
||||||
|
sudo newszxcn-email reset-password
|
||||||
```
|
```
|
||||||
|
|
||||||
命令行更新会创建完整回滚快照、校验 SQLite 数据库备份、拉取最新镜像并执行健康检查。`rollback` 命令会先备份当前数据库并要求确认,然后恢复上次更新前的镜像、数据库、Compose、环境、安装脚本和 Nginx 配置。回滚镜像会保持锁定,下一次执行更新时解除。
|
命令行更新会创建完整回滚快照、校验 SQLite 数据库备份、拉取最新镜像并执行健康检查。`rollback` 命令会先备份当前数据库并要求确认,然后恢复上次更新前的镜像、数据库、Compose、环境、安装脚本和 Nginx 配置。回滚镜像会保持锁定,下一次执行更新时解除。
|
||||||
|
|
||||||
`guide` 命令会读取当前安装地址、管理员用户名、证书到期时间和 acme.sh 续期状态,重新生成仅 root 可读的 `/root/newszxcn-email-guide.txt`。
|
`guide` 命令会读取当前安装地址、管理员用户名、证书到期时间和 acme.sh 续期状态,重新生成仅 root 可读的 `/root/newszxcn-email-guide.txt`。
|
||||||
|
|
||||||
|
`credentials` 显示安装或最近一次命令行重置时记录的管理员登录信息。数据库只保存 bcrypt 密码哈希,无法反向查看真实密码;若管理员后来在网页修改过密码,记录值可能已经失效。忘记密码时执行 `reset-password`,脚本会先备份并校验数据库,然后重置配置管理员的统一登录密码,同时同步该管理员名下邮箱的 SMTP/IMAP 密码。该操作不会修改普通用户或其邮箱。
|
||||||
|
|
||||||
超级管理员也可以点击管理后台侧栏中的版本号,在版本更新页面检查并安装新版本。
|
超级管理员也可以点击管理后台侧栏中的版本号,在版本更新页面检查并安装新版本。
|
||||||
|
|
||||||
## 必要端口
|
## 必要端口
|
||||||
|
|||||||
+129
-4
@@ -35,6 +35,8 @@ NewSzxcn Email 管理命令
|
|||||||
certificate 申请或续期自动模式的 SSL 证书
|
certificate 申请或续期自动模式的 SSL 证书
|
||||||
rollback 回滚到上次更新前版本
|
rollback 回滚到上次更新前版本
|
||||||
guide 显示并更新 NewSzxcn 邮箱指南
|
guide 显示并更新 NewSzxcn 邮箱指南
|
||||||
|
credentials 查看管理员登录信息和记录密码
|
||||||
|
reset-password 重置管理员统一登录密码(含名下邮箱)
|
||||||
uninstall 停止并移除容器,保留邮件与配置
|
uninstall 停止并移除容器,保留邮件与配置
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -233,7 +235,7 @@ prompt_choice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prompt_menu_choice() {
|
prompt_menu_choice() {
|
||||||
local default_value="$1" max_value="${2:-10}" value="${LANQIN_MENU_ACTION:-}"
|
local default_value="$1" max_value="${2:-12}" value="${LANQIN_MENU_ACTION:-}"
|
||||||
if [[ -z "${value}" ]] && ! has_tty; then
|
if [[ -z "${value}" ]] && ! has_tty; then
|
||||||
fail "非交互环境请直接使用 install、update、status 等子命令。"
|
fail "非交互环境请直接使用 install、update、status 等子命令。"
|
||||||
fi
|
fi
|
||||||
@@ -320,6 +322,50 @@ prompt_admin_password() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_reset_password() {
|
||||||
|
local password="${LANQIN_RESET_PASSWORD:-}" confirm=""
|
||||||
|
local safe_password_re='^[A-Za-z0-9][A-Za-z0-9._!@#%+,=:;?*/()^-]*$'
|
||||||
|
if [[ -n "${password}" ]]; then
|
||||||
|
[[ ${#password} -ge 6 ]] || fail "新密码至少需要 6 个字符。"
|
||||||
|
[[ "${password}" =~ ${safe_password_re} ]] || fail "新密码包含安装配置不支持的字符。"
|
||||||
|
printf '%s' "${password}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! has_tty; then
|
||||||
|
password="$(random_admin_password)"
|
||||||
|
prompt_text "[提示] 已自动生成 12 位新密码:${password}\n"
|
||||||
|
printf '%s' "${password}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
while true; do
|
||||||
|
read -r -s -p "新密码(回车自动生成 12 位,或输入至少 6 位): " password </dev/tty
|
||||||
|
printf '\n' >/dev/tty
|
||||||
|
if [[ -z "${password}" ]]; then
|
||||||
|
password="$(random_admin_password)"
|
||||||
|
prompt_text "[提示] 已自动生成 12 位新密码:${password}\n"
|
||||||
|
printf '%s' "${password}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ${#password} -lt 6 ]]; then
|
||||||
|
prompt_text "[提示] 新密码至少需要 6 个字符。\n"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ ! "${password}" =~ ${safe_password_re} ]]; then
|
||||||
|
prompt_text "[提示] 密码必须以字母或数字开头,只能使用字母、数字和常用符号。\n"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
read -r -s -p "再次输入新密码: " confirm </dev/tty
|
||||||
|
printf '\n' >/dev/tty
|
||||||
|
if [[ "${password}" != "${confirm}" ]]; then
|
||||||
|
prompt_text "[提示] 两次输入的密码不一致,请重新输入。\n"
|
||||||
|
password=""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
printf '%s' "${password}"
|
||||||
|
return
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
configure_first_install() {
|
configure_first_install() {
|
||||||
if [[ -f "${INSTALL_DIR}/.env" ]]; then
|
if [[ -f "${INSTALL_DIR}/.env" ]]; then
|
||||||
return
|
return
|
||||||
@@ -1030,6 +1076,8 @@ SSL 证书:有效期至 ${certificate_expiry}
|
|||||||
查看状态:newszxcn-email status
|
查看状态:newszxcn-email status
|
||||||
查看日志:newszxcn-email logs
|
查看日志:newszxcn-email logs
|
||||||
恢复版本:newszxcn-email rollback
|
恢复版本:newszxcn-email rollback
|
||||||
|
查看管理员登录信息:newszxcn-email credentials
|
||||||
|
重置管理员密码:newszxcn-email reset-password
|
||||||
|
|
||||||
公开教程:
|
公开教程:
|
||||||
https://github.com/zxyszx/NewSzxcn-Email/blob/main/docs/GUIDE.md
|
https://github.com/zxyszx/NewSzxcn-Email/blob/main/docs/GUIDE.md
|
||||||
@@ -1044,6 +1092,77 @@ do_guide() {
|
|||||||
success "指南已更新并保存到 ${GUIDE_FILE}。"
|
success "指南已更新并保存到 ${GUIDE_FILE}。"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do_show_admin_credentials() {
|
||||||
|
[[ -f "${INSTALL_DIR}/.env" ]] || fail "尚未安装。"
|
||||||
|
local username password public_url
|
||||||
|
username="$(env_value LANQIN_ADMIN_USERNAME || true)"
|
||||||
|
password="$(env_value LANQIN_ADMIN_PASSWORD || true)"
|
||||||
|
public_url="$(env_value LANQIN_PUBLIC_BASE_URL || true)"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
NewSzxcn 管理员登录信息
|
||||||
|
==================================================
|
||||||
|
登录地址:${public_url:-未记录}
|
||||||
|
管理员用户名:${username:-admin}
|
||||||
|
记录密码:${password:-未记录}
|
||||||
|
==================================================
|
||||||
|
EOF
|
||||||
|
warn "当前密码采用 bcrypt 加密,无法从数据库反向查看。这里显示的是安装或最近一次命令行重置时记录的密码;如果之后在网页修改过密码,该记录可能已经失效。"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_admin_password_hash() {
|
||||||
|
local password="$1" hash
|
||||||
|
hash="$(compose exec -T lanqin-email doveadm pw -s BLF-CRYPT -r 10 -p "${password}" 2>/dev/null | tail -n 1)"
|
||||||
|
hash="${hash#\{BLF-CRYPT\}}"
|
||||||
|
[[ "${hash}" =~ ^\$2[aby]\$[0-9]{2}\$.{53}$ ]] || return 1
|
||||||
|
printf '%s' "${hash}"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_reset_admin_password() {
|
||||||
|
[[ -f "${INSTALL_DIR}/.env" ]] || fail "尚未安装。"
|
||||||
|
local username password user_id hash image timestamp backup env_backup result user_changes mailbox_changes
|
||||||
|
ensure_docker
|
||||||
|
username="$(env_value LANQIN_ADMIN_USERNAME || true)"
|
||||||
|
username="${username:-admin}"
|
||||||
|
[[ "${username}" =~ ^[A-Za-z0-9][A-Za-z0-9._%+-]{1,79}$ ]] || fail "管理员用户名配置无效,无法安全重置。"
|
||||||
|
user_id="$(compose exec -T lanqin-email sqlite3 -batch -noheader /data/lanqin.db "SELECT id FROM users WHERE login_name='${username}' AND role='admin' LIMIT 1;" | tr -d '\r\n')"
|
||||||
|
[[ "${user_id}" =~ ^[A-Za-z0-9_-]+$ ]] || fail "没有找到管理员账号 ${username}。"
|
||||||
|
password="$(prompt_reset_password)"
|
||||||
|
hash="$(generate_admin_password_hash "${password}")" || fail "无法生成安全密码哈希,管理员密码未修改。"
|
||||||
|
image="$(current_image_id || true)"
|
||||||
|
[[ -n "${image}" ]] || fail "无法确定当前镜像,管理员密码未修改。"
|
||||||
|
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
backup="${INSTALL_DIR}/data/backups/password-reset-${timestamp}.db"
|
||||||
|
backup_database "${backup}" "${image}" || fail "数据库备份失败,管理员密码未修改。"
|
||||||
|
|
||||||
|
env_backup="$(mktemp)"
|
||||||
|
install -m 0600 "${INSTALL_DIR}/.env" "${env_backup}"
|
||||||
|
if ! set_env LANQIN_ADMIN_PASSWORD "${password}"; then
|
||||||
|
rm -f "${env_backup}"
|
||||||
|
fail "管理员密码记录更新失败,数据库未修改。"
|
||||||
|
fi
|
||||||
|
chmod 0600 "${INSTALL_DIR}/.env"
|
||||||
|
if ! result="$(compose exec -T lanqin-email sqlite3 -batch -noheader /data/lanqin.db "BEGIN IMMEDIATE; UPDATE users SET password_hash='${hash}', updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id='${user_id}' AND role='admin'; SELECT 'user=' || changes(); UPDATE mailboxes SET password_hash='${hash}', updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE user_id='${user_id}' AND EXISTS (SELECT 1 FROM users WHERE id='${user_id}' AND role='admin'); SELECT 'mailboxes=' || changes(); COMMIT;" | tr -d '\r')"; then
|
||||||
|
install -m 0600 "${env_backup}" "${INSTALL_DIR}/.env"
|
||||||
|
rm -f "${env_backup}"
|
||||||
|
fail "管理员密码写入失败,已恢复原密码记录。"
|
||||||
|
fi
|
||||||
|
user_changes="$(printf '%s\n' "${result}" | sed -n 's/^user=//p' | tail -n 1)"
|
||||||
|
mailbox_changes="$(printf '%s\n' "${result}" | sed -n 's/^mailboxes=//p' | tail -n 1)"
|
||||||
|
if [[ "${user_changes}" != "1" || ! "${mailbox_changes}" =~ ^[0-9]+$ ]]; then
|
||||||
|
install -m 0600 "${env_backup}" "${INSTALL_DIR}/.env"
|
||||||
|
rm -f "${env_backup}"
|
||||||
|
fail "管理员账号不存在或身份已变化,密码未修改;已恢复原密码记录。"
|
||||||
|
fi
|
||||||
|
rm -f "${env_backup}"
|
||||||
|
success "管理员 ${username} 的统一登录密码已重置。"
|
||||||
|
printf '新密码:%s\n' "${password}"
|
||||||
|
log "重置前数据库备份:${backup}"
|
||||||
|
log "已同步 ${mailbox_changes} 个管理员邮箱的 SMTP/IMAP 密码。"
|
||||||
|
warn "此次操作只修改管理员账号及其名下邮箱,不会修改普通用户或其邮箱密码。"
|
||||||
|
}
|
||||||
|
|
||||||
do_status() {
|
do_status() {
|
||||||
[[ -f "${INSTALL_DIR}/docker-compose.yml" ]] || fail "尚未安装。"
|
[[ -f "${INSTALL_DIR}/docker-compose.yml" ]] || fail "尚未安装。"
|
||||||
compose ps
|
compose ps
|
||||||
@@ -1181,11 +1300,13 @@ do_menu() {
|
|||||||
prompt_text ' 7. 申请、检查或续期 SSL 证书\n'
|
prompt_text ' 7. 申请、检查或续期 SSL 证书\n'
|
||||||
prompt_text ' 8. 回滚到上次更新前版本\n'
|
prompt_text ' 8. 回滚到上次更新前版本\n'
|
||||||
prompt_text ' 9. NewSzxcn 邮箱指南\n'
|
prompt_text ' 9. NewSzxcn 邮箱指南\n'
|
||||||
prompt_text ' 10. 卸载服务(保留数据)\n'
|
prompt_text ' 10. 查看管理员登录信息\n'
|
||||||
|
prompt_text ' 11. 重置管理员统一登录密码\n'
|
||||||
|
prompt_text ' 12. 卸载服务(保留数据)\n'
|
||||||
prompt_text ' 0. 退出\n'
|
prompt_text ' 0. 退出\n'
|
||||||
prompt_text '==================================================\n'
|
prompt_text '==================================================\n'
|
||||||
|
|
||||||
choice="$(prompt_menu_choice "${default_choice}" "10")"
|
choice="$(prompt_menu_choice "${default_choice}" "12")"
|
||||||
if [[ "${choice}" != "0" && "${choice}" != "1" && "${installed}" != "true" ]]; then
|
if [[ "${choice}" != "0" && "${choice}" != "1" && "${installed}" != "true" ]]; then
|
||||||
fail "尚未安装,请先选择 1。"
|
fail "尚未安装,请先选择 1。"
|
||||||
fi
|
fi
|
||||||
@@ -1201,7 +1322,9 @@ do_menu() {
|
|||||||
7) do_certificate ;;
|
7) do_certificate ;;
|
||||||
8) ensure_docker; do_rollback ;;
|
8) ensure_docker; do_rollback ;;
|
||||||
9) do_guide ;;
|
9) do_guide ;;
|
||||||
10) ensure_docker; do_uninstall ;;
|
10) do_show_admin_credentials ;;
|
||||||
|
11) do_reset_admin_password ;;
|
||||||
|
12) ensure_docker; do_uninstall ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,6 +1351,8 @@ case "${COMMAND}" in
|
|||||||
certificate) require_root; require_curl; do_certificate ;;
|
certificate) require_root; require_curl; do_certificate ;;
|
||||||
rollback) require_root; require_curl; ensure_docker; do_rollback ;;
|
rollback) require_root; require_curl; ensure_docker; do_rollback ;;
|
||||||
guide) require_root; require_curl; do_guide ;;
|
guide) require_root; require_curl; do_guide ;;
|
||||||
|
credentials) require_root; require_curl; do_show_admin_credentials ;;
|
||||||
|
reset-password) require_root; require_curl; do_reset_admin_password ;;
|
||||||
uninstall) require_root; require_curl; ensure_docker; do_uninstall ;;
|
uninstall) require_root; require_curl; ensure_docker; do_uninstall ;;
|
||||||
*) usage; fail "未知命令:${COMMAND}" ;;
|
*) usage; fail "未知命令:${COMMAND}" ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+76
-3
@@ -36,6 +36,11 @@ test_password_validation() {
|
|||||||
if (LANQIN_ADMIN_PASSWORD="#abc123" prompt_admin_password >/dev/null 2>&1); then
|
if (LANQIN_ADMIN_PASSWORD="#abc123" prompt_admin_password >/dev/null 2>&1); then
|
||||||
fail_test "password beginning with an env-file comment marker accepted"
|
fail_test "password beginning with an env-file comment marker accepted"
|
||||||
fi
|
fi
|
||||||
|
LANQIN_RESET_PASSWORD="reset1"
|
||||||
|
assert_eq "reset1" "$(prompt_reset_password)" "six-character reset password"
|
||||||
|
if (LANQIN_RESET_PASSWORD="reset" prompt_reset_password >/dev/null 2>&1); then
|
||||||
|
fail_test "five-character reset password accepted"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
test_install_configuration() {
|
test_install_configuration() {
|
||||||
@@ -114,14 +119,79 @@ test_menu_choice() {
|
|||||||
assert_eq "0" "$(prompt_menu_choice 1)" "menu exit action"
|
assert_eq "0" "$(prompt_menu_choice 1)" "menu exit action"
|
||||||
export LANQIN_MENU_ACTION=1
|
export LANQIN_MENU_ACTION=1
|
||||||
assert_eq "1" "$(prompt_menu_choice 2)" "menu install action"
|
assert_eq "1" "$(prompt_menu_choice 2)" "menu install action"
|
||||||
export LANQIN_MENU_ACTION=10
|
export LANQIN_MENU_ACTION=12
|
||||||
assert_eq "10" "$(prompt_menu_choice 1 10)" "menu uninstall action"
|
assert_eq "12" "$(prompt_menu_choice 1 12)" "menu uninstall action"
|
||||||
if (has_tty() { return 1; }; LANQIN_MENU_ACTION=11 prompt_menu_choice 1 10 >/dev/null 2>&1); then
|
if (has_tty() { return 1; }; LANQIN_MENU_ACTION=13 prompt_menu_choice 1 12 >/dev/null 2>&1); then
|
||||||
fail_test "out-of-range menu action accepted"
|
fail_test "out-of-range menu action accepted"
|
||||||
fi
|
fi
|
||||||
unset LANQIN_MENU_ACTION
|
unset LANQIN_MENU_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_admin_credentials() (
|
||||||
|
local temp_dir output
|
||||||
|
temp_dir="$(mktemp -d)"
|
||||||
|
INSTALL_DIR="${temp_dir}/install"
|
||||||
|
mkdir -p "${INSTALL_DIR}"
|
||||||
|
cat > "${INSTALL_DIR}/.env" <<'EOF'
|
||||||
|
LANQIN_PUBLIC_BASE_URL=https://mail.example.com
|
||||||
|
LANQIN_ADMIN_USERNAME=admin
|
||||||
|
LANQIN_ADMIN_PASSWORD=recorded-password
|
||||||
|
EOF
|
||||||
|
output="$(do_show_admin_credentials 2>&1)"
|
||||||
|
[[ "${output}" == *'登录地址:https://mail.example.com'* ]] || fail_test "administrator login URL missing"
|
||||||
|
[[ "${output}" == *'管理员用户名:admin'* ]] || fail_test "administrator username missing"
|
||||||
|
[[ "${output}" == *'记录密码:recorded-password'* ]] || fail_test "recorded administrator password missing"
|
||||||
|
[[ "${output}" == *'无法从数据库反向查看'* ]] || fail_test "password hash warning missing"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_admin_password_hash_parsing() (
|
||||||
|
compose() {
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
printf '{BLF-CRYPT}$2y$10$123456789012345678901u1234567890123456789012345678901\n'
|
||||||
|
}
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
assert_eq '$2y$10$123456789012345678901u1234567890123456789012345678901' "$(generate_admin_password_hash 'unused')" "Dovecot bcrypt hash parsing"
|
||||||
|
)
|
||||||
|
|
||||||
|
test_admin_password_reset_only_updates_admin_account() (
|
||||||
|
local temp_dir compose_calls backup_path
|
||||||
|
temp_dir="$(mktemp -d)"
|
||||||
|
INSTALL_DIR="${temp_dir}/install"
|
||||||
|
compose_calls="${temp_dir}/compose-calls"
|
||||||
|
mkdir -p "${INSTALL_DIR}/data/backups"
|
||||||
|
cat > "${INSTALL_DIR}/.env" <<'EOF'
|
||||||
|
LANQIN_ADMIN_USERNAME=admin
|
||||||
|
LANQIN_ADMIN_PASSWORD=old-password
|
||||||
|
EOF
|
||||||
|
printf 'database\n' > "${INSTALL_DIR}/data/lanqin.db"
|
||||||
|
|
||||||
|
ensure_docker() { return 0; }
|
||||||
|
current_image_id() { printf 'sha256:test-image\n'; }
|
||||||
|
backup_database() {
|
||||||
|
backup_path="$1"
|
||||||
|
printf 'backup\n' > "${backup_path}"
|
||||||
|
}
|
||||||
|
prompt_reset_password() { printf 'new-password'; }
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
generate_admin_password_hash() { printf '$2y$10$123456789012345678901u1234567890123456789012345678901'; }
|
||||||
|
compose() {
|
||||||
|
printf '%s\n' "$*" >> "${compose_calls}"
|
||||||
|
if [[ "$*" == *'SELECT id FROM users'* ]]; then
|
||||||
|
printf 'admin-user-id\n'
|
||||||
|
elif [[ "$*" == *'UPDATE users SET password_hash'* ]]; then
|
||||||
|
printf 'user=1\nmailboxes=2\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
do_reset_admin_password >/dev/null
|
||||||
|
assert_eq "new-password" "$(env_value LANQIN_ADMIN_PASSWORD)" "recorded reset password"
|
||||||
|
[[ -s "${backup_path}" ]] || fail_test "password reset database backup missing"
|
||||||
|
grep -Fq "login_name='admin' AND role='admin'" "${compose_calls}" || fail_test "administrator lookup is not role restricted"
|
||||||
|
grep -Fq "UPDATE users SET password_hash=" "${compose_calls}" || fail_test "administrator user password was not updated"
|
||||||
|
grep -Fq "UPDATE mailboxes SET password_hash=" "${compose_calls}" || fail_test "administrator mailbox passwords were not synchronized"
|
||||||
|
grep -Fq "WHERE user_id='admin-user-id'" "${compose_calls}" || fail_test "mailbox password update is not restricted to the administrator"
|
||||||
|
)
|
||||||
|
|
||||||
test_offline_database_backup() (
|
test_offline_database_backup() (
|
||||||
local temp_dir destination
|
local temp_dir destination
|
||||||
temp_dir="$(mktemp -d)"
|
temp_dir="$(mktemp -d)"
|
||||||
@@ -439,6 +509,9 @@ test_nginx_configuration
|
|||||||
test_compose_configuration
|
test_compose_configuration
|
||||||
test_legacy_configuration_is_preserved
|
test_legacy_configuration_is_preserved
|
||||||
test_menu_choice
|
test_menu_choice
|
||||||
|
test_admin_credentials
|
||||||
|
test_admin_password_hash_parsing
|
||||||
|
test_admin_password_reset_only_updates_admin_account
|
||||||
test_offline_database_backup
|
test_offline_database_backup
|
||||||
test_guide_generation
|
test_guide_generation
|
||||||
test_acme_cron_detection
|
test_acme_cron_detection
|
||||||
|
|||||||
Reference in New Issue
Block a user