This commit is contained in:
@@ -15,6 +15,9 @@ dist/
|
|||||||
output/
|
output/
|
||||||
web/public/111favicon.png
|
web/public/111favicon.png
|
||||||
web/src/assets/brand/logo00.png
|
web/src/assets/brand/logo00.png
|
||||||
|
fnos/*.fpk
|
||||||
|
fnos/.build/
|
||||||
|
fnos/dist/
|
||||||
# scripts/ 下的本地辅助脚本不入库,但随仓库分发的 agent 安装脚本需要跟踪
|
# scripts/ 下的本地辅助脚本不入库,但随仓库分发的 agent 安装脚本需要跟踪
|
||||||
scripts/*
|
scripts/*
|
||||||
!scripts/agent-install.sh
|
!scripts/agent-install.sh
|
||||||
|
|||||||
@@ -1001,6 +1001,25 @@ Havline 适合做飞牛 Docker 应用,不适合首发做成 CGI 或原生应
|
|||||||
首发使用飞牛应用中心升级。`manifest.changelog` 应面向用户简洁表达本次变更;
|
首发使用飞牛应用中心升级。`manifest.changelog` 应面向用户简洁表达本次变更;
|
||||||
Havline 内部升级页面、`havline-updater` sidecar 和 Docker Socket 默认不进入 fnOS 包。
|
Havline 内部升级页面、`havline-updater` sidecar 和 Docker Socket 默认不进入 fnOS 包。
|
||||||
|
|
||||||
|
11. **端口入口和容器端口必须区分**
|
||||||
|
|
||||||
|
`app/ui/config` 的 `port` 支持使用向导变量,因此管理入口可以写成:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"port": "${wizard_service_port}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose 中应把宿主机端口映射到 Havline 容器内的固定端口:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "${wizard_service_port}:6893"
|
||||||
|
- "${wizard_nginx_http_port}:80"
|
||||||
|
- "${wizard_nginx_https_port}:443"
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把 `18080` 或 `9443` 当成容器内部监听端口。它们是宿主机端口,容器内仍然是 `80/443`。
|
||||||
|
|
||||||
### 19.3 建议的首发实施顺序
|
### 19.3 建议的首发实施顺序
|
||||||
|
|
||||||
阶段 1 只做“能安装、能打开、能保留数据”:
|
阶段 1 只做“能安装、能打开、能保留数据”:
|
||||||
@@ -1014,6 +1033,14 @@ Havline 适合做飞牛 Docker 应用,不适合首发做成 CGI 或原生应
|
|||||||
7. 构建 amd64 / arm64 镜像。
|
7. 构建 amd64 / arm64 镜像。
|
||||||
8. 用 `fnpack build` 打包并在飞牛测试设备安装验证。
|
8. 用 `fnpack build` 打包并在飞牛测试设备安装验证。
|
||||||
|
|
||||||
|
当前已经在仓库内生成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fnos/havline/
|
||||||
|
```
|
||||||
|
|
||||||
|
并补上了首发骨架所需的基础配置。
|
||||||
|
|
||||||
阶段 1 暂不实现:
|
阶段 1 暂不实现:
|
||||||
|
|
||||||
- Havline 自带 updater;
|
- Havline 自带 updater;
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Havline fnOS 应用打包
|
||||||
|
|
||||||
|
本目录保存 Havline 的飞牛 fnOS 应用工程。
|
||||||
|
|
||||||
|
- 应用工程:[`havline/`](havline/)
|
||||||
|
- 官方工具:`fnpack`
|
||||||
|
- 当前工具版本:`1.2.3`
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
已经完成:
|
||||||
|
|
||||||
|
- 使用 `fnpack create havline --template docker` 生成官方 Docker 应用骨架;
|
||||||
|
- 配置 `manifest`、应用入口、Docker Compose、权限和资源;
|
||||||
|
- 增加安装向导、配置向导和卸载向导;
|
||||||
|
- 增加安装、升级、配置和卸载生命周期脚本;
|
||||||
|
- 支持通过环境变量 `HAVLINE_INITIAL_ADMIN_PASSWORD` 初始化管理员。
|
||||||
|
|
||||||
|
仍需完成:
|
||||||
|
|
||||||
|
- 将 `app/docker/docker-compose.yaml` 中的 `ghcr.io/your-org/havline` 替换为实际多架构镜像地址;
|
||||||
|
- 将模板图标替换为 Havline 正式图标;
|
||||||
|
- 在飞牛测试设备验证安装、启动、入口、升级和卸载;
|
||||||
|
- 验证 amd64 与 arm64 镜像均可用。
|
||||||
|
|
||||||
|
## 打包
|
||||||
|
|
||||||
|
Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd F:\Fonu-main\havline
|
||||||
|
.\fnos\build.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会先复制一份不含 `.DS_Store` 的干净工程,再调用 `fnpack` 打包,输出到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fnos/dist/havline-<version>.fpk
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `fnpack` 已在 PATH 中,也可以传入路径:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\fnos\build.ps1 -FnpackPath fnpack
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
appcenter-cli install-fpk havline.fpk
|
||||||
|
```
|
||||||
|
|
||||||
|
安装后检查:
|
||||||
|
|
||||||
|
1. 应用中心显示 Havline;
|
||||||
|
2. Docker 镜像可以拉取;
|
||||||
|
3. Havline 容器能够启动;
|
||||||
|
4. 桌面入口可以打开管理界面;
|
||||||
|
5. 数据写入 `${TRIM_PKGVAR}`;
|
||||||
|
6. 卸载默认保留数据。
|
||||||
|
|
||||||
|
## 当前默认端口
|
||||||
|
|
||||||
|
| 用途 | 安装向导默认值 | 容器端口 |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| 管理界面 | `6893` | `6893` |
|
||||||
|
| HTTP 反向代理 | `18080` | `80` |
|
||||||
|
| HTTPS 反向代理 | `9443` | `443` |
|
||||||
|
|
||||||
|
## 首发边界
|
||||||
|
|
||||||
|
- 不挂载 Docker Socket。
|
||||||
|
- 不启动 Havline 自带 updater。
|
||||||
|
- 不使用 host 网络。
|
||||||
|
- 不共享数据库、证书、密钥、Nginx、FRP 和 cloudflared 目录。
|
||||||
|
- 应用中心负责安装、升级、启动、停止和卸载。
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
param(
|
||||||
|
[string]$FnpackPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
|
$project = Join-Path $root "fnos\havline"
|
||||||
|
$version = (Get-Content -Raw (Join-Path $root "VERSION")).Trim()
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $project)) {
|
||||||
|
throw "找不到飞牛应用工程:$project"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $FnpackPath) {
|
||||||
|
$localTool = Join-Path (Split-Path $root -Parent) "fnos\fnpack.exe"
|
||||||
|
$FnpackPath = if (Test-Path -LiteralPath $localTool) { $localTool } else { "fnpack" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$stamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||||
|
$stage = Join-Path $PSScriptRoot ".build\havline-$stamp"
|
||||||
|
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||||
|
robocopy $project $stage /E /XF .DS_Store /NFL /NDL /NJH /NJS /NP | Out-Null
|
||||||
|
if ($LASTEXITCODE -ge 8) {
|
||||||
|
throw "复制飞牛应用工程失败,robocopy 退出码:$LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $PSScriptRoot
|
||||||
|
try {
|
||||||
|
& $FnpackPath build -d $stage
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "fnpack build 失败,退出码:$LASTEXITCODE"
|
||||||
|
}
|
||||||
|
$output = Join-Path $PSScriptRoot "havline.fpk"
|
||||||
|
$dist = Join-Path $PSScriptRoot "dist"
|
||||||
|
New-Item -ItemType Directory -Force $dist | Out-Null
|
||||||
|
Copy-Item -LiteralPath $output -Destination (Join-Path $dist "havline-$version.fpk") -Force
|
||||||
|
Write-Output "FPK: $(Join-Path $dist "havline-$version.fpk")"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
havline:
|
||||||
|
# TODO: 发布多架构镜像后,把 your-org 替换为实际镜像命名空间。
|
||||||
|
image: ghcr.io/your-org/havline:${TRIM_APPVER}
|
||||||
|
container_name: havline
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${wizard_service_port}:6893"
|
||||||
|
- "${wizard_nginx_http_port}:80"
|
||||||
|
- "${wizard_nginx_https_port}:443"
|
||||||
|
volumes:
|
||||||
|
- "${TRIM_PKGVAR}:/data"
|
||||||
|
environment:
|
||||||
|
HAVLINE_DATA_DIR: /data
|
||||||
|
HAVLINE_LISTEN: ":6893"
|
||||||
|
HAVLINE_NGINX_HTTP_PORT: "80"
|
||||||
|
HAVLINE_NGINX_HTTPS_PORT: "443"
|
||||||
|
HAVLINE_SESSION_SECRET: ""
|
||||||
|
HAVLINE_UPDATER_SOCKET: ""
|
||||||
|
HAVLINE_INITIAL_ADMIN_PASSWORD: "${wizard_initial_admin_password}"
|
||||||
|
TZ: "${wizard_timezone}"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
".url": {
|
||||||
|
"havline.main": {
|
||||||
|
"title": "Havline",
|
||||||
|
"icon": "images/icon_{0}.png",
|
||||||
|
"type": "iframe",
|
||||||
|
"protocol": "http",
|
||||||
|
"port": "${wizard_service_port}",
|
||||||
|
"url": "/",
|
||||||
|
"allUsers": false,
|
||||||
|
"control": {
|
||||||
|
"accessPerm": "readonly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
for var in wizard_service_port wizard_nginx_http_port wizard_nginx_https_port; do
|
||||||
|
value="${!var:-}"
|
||||||
|
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "$var 必须是数字端口" > "$TRIM_TEMP_LOGFILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
mkdir -p "$TRIM_PKGVAR" "$TRIM_PKGVAR/backups"
|
||||||
|
chown -R "$TRIM_USERNAME:$TRIM_GROUPNAME" "$TRIM_PKGVAR"
|
||||||
|
|
||||||
|
echo "Havline 数据目录已准备:$TRIM_PKGVAR"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
CONTAINER_NAME="havline"
|
||||||
|
LOG_FILE="${TRIM_TEMP_LOGFILE:-/dev/stderr}"
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo "$1" > "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_running() {
|
||||||
|
docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -q '"Status": "running"'
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start|stop)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if is_running; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 3
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unknown command: ${1:-}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if [ "${wizard_uninstall_mode:-keep}" = "purge" ]; then
|
||||||
|
rm -rf "$TRIM_PKGVAR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -f "$TRIM_PKGVAR/havline.db" ] && [ ! -r "$TRIM_PKGVAR/havline.db" ]; then
|
||||||
|
echo "数据库不可读:$TRIM_PKGVAR/havline.db" > "$TRIM_TEMP_LOGFILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$TRIM_PKGVAR/session_secret" ] && [ ! -r "$TRIM_PKGVAR/session_secret" ]; then
|
||||||
|
echo "会话密钥不可读:$TRIM_PKGVAR/session_secret" > "$TRIM_TEMP_LOGFILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"defaults": {
|
||||||
|
"run-as": "package"
|
||||||
|
},
|
||||||
|
"username": "havline",
|
||||||
|
"groupname": "havline"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"docker-project": {
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"name": "havline",
|
||||||
|
"path": "docker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data-share": {
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"name": "havline/backups"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
appname = havline
|
||||||
|
version = 1.9.0
|
||||||
|
display_name = Havline
|
||||||
|
desc = NAS 公网访问管理:反向代理、DDNS、HTTPS 证书、FRP、Cloudflare Tunnel 与一键升级
|
||||||
|
platform = all
|
||||||
|
source = thirdparty
|
||||||
|
maintainer = Havline
|
||||||
|
maintainer_url = http://45.207.223.190:10082/mrlee/Havline
|
||||||
|
distributor = Havline
|
||||||
|
desktop_uidir = ui
|
||||||
|
desktop_applaunchname = havline.main
|
||||||
|
checkport = false
|
||||||
|
ctl_stop = true
|
||||||
|
changelog = 首发飞牛应用:支持安装向导、端口配置、数据持久化和应用中心升级
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"stepTitle": "端口设置",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"field": "wizard_nginx_http_port",
|
||||||
|
"label": "HTTP 反向代理端口",
|
||||||
|
"initValue": "18080",
|
||||||
|
"rules": [
|
||||||
|
{ "required": true, "message": "请输入 HTTP 端口" },
|
||||||
|
{ "pattern": "^[0-9]+$", "message": "端口必须是数字" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"field": "wizard_nginx_https_port",
|
||||||
|
"label": "HTTPS 反向代理端口",
|
||||||
|
"initValue": "9443",
|
||||||
|
"rules": [
|
||||||
|
{ "required": true, "message": "请输入 HTTPS 端口" },
|
||||||
|
{ "pattern": "^[0-9]+$", "message": "端口必须是数字" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stepTitle": "基础设置",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"field": "wizard_timezone",
|
||||||
|
"label": "时区",
|
||||||
|
"initValue": "Asia/Shanghai",
|
||||||
|
"options": [
|
||||||
|
{ "label": "Asia/Shanghai", "value": "Asia/Shanghai" },
|
||||||
|
{ "label": "UTC", "value": "UTC" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"stepTitle": "端口设置",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"field": "wizard_service_port",
|
||||||
|
"label": "Havline 管理端口",
|
||||||
|
"initValue": "6893",
|
||||||
|
"rules": [
|
||||||
|
{ "required": true, "message": "请输入管理端口" },
|
||||||
|
{ "pattern": "^[0-9]+$", "message": "端口必须是数字" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"field": "wizard_nginx_http_port",
|
||||||
|
"label": "HTTP 反向代理端口",
|
||||||
|
"initValue": "18080",
|
||||||
|
"rules": [
|
||||||
|
{ "required": true, "message": "请输入 HTTP 端口" },
|
||||||
|
{ "pattern": "^[0-9]+$", "message": "端口必须是数字" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"field": "wizard_nginx_https_port",
|
||||||
|
"label": "HTTPS 反向代理端口",
|
||||||
|
"initValue": "9443",
|
||||||
|
"rules": [
|
||||||
|
{ "required": true, "message": "请输入 HTTPS 端口" },
|
||||||
|
{ "pattern": "^[0-9]+$", "message": "端口必须是数字" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stepTitle": "管理员初始化",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"field": "wizard_initial_admin_password",
|
||||||
|
"label": "初始管理员密码",
|
||||||
|
"rules": [
|
||||||
|
{ "required": true, "message": "请输入初始管理员密码" },
|
||||||
|
{ "min": 8, "message": "密码至少 8 位" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tips",
|
||||||
|
"helpText": "首次启动时使用该密码创建 admin 管理员。登录后请在系统设置中修改密码。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stepTitle": "基础设置",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"field": "wizard_timezone",
|
||||||
|
"label": "时区",
|
||||||
|
"initValue": "Asia/Shanghai",
|
||||||
|
"options": [
|
||||||
|
{ "label": "Asia/Shanghai", "value": "Asia/Shanghai" },
|
||||||
|
{ "label": "UTC", "value": "UTC" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"stepTitle": "卸载选项",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "radio",
|
||||||
|
"field": "wizard_uninstall_mode",
|
||||||
|
"label": "数据处理",
|
||||||
|
"initValue": "keep",
|
||||||
|
"options": [
|
||||||
|
{ "label": "保留数据(推荐)", "value": "keep" },
|
||||||
|
{ "label": "删除全部数据", "value": "purge" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tips",
|
||||||
|
"helpText": "删除数据将移除数据库、证书、Nginx 配置、FRP 与 Cloudflare 配置,且无法恢复。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
+26
-9
@@ -8,6 +8,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -45,20 +47,35 @@ func (s *Service) EnsureDefaultAdmin(ctx context.Context, log *slog.Logger) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
password, err := generatePassword(16)
|
password := strings.TrimSpace(os.Getenv("HAVLINE_INITIAL_ADMIN_PASSWORD"))
|
||||||
if err != nil {
|
generated := false
|
||||||
return err
|
if password == "" {
|
||||||
|
var err error
|
||||||
|
password, err = generatePassword(16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
generated = true
|
||||||
}
|
}
|
||||||
if err := s.Setup(ctx, defaultAdminUsername, password); err != nil {
|
if err := s.Setup(ctx, defaultAdminUsername, password); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info(
|
if generated {
|
||||||
"首次启动已创建管理员账户,请使用以下凭据登录,并在「设置」中尽快修改密码",
|
log.Info(
|
||||||
"module", "SYSTEM",
|
"首次启动已创建管理员账户,请使用以下凭据登录,并在「设置」中尽快修改密码",
|
||||||
"username", defaultAdminUsername,
|
"module", "SYSTEM",
|
||||||
"password", password,
|
"username", defaultAdminUsername,
|
||||||
)
|
"password", password,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Info(
|
||||||
|
"首次启动已使用 HAVLINE_INITIAL_ADMIN_PASSWORD 创建管理员账户,请在登录后尽快修改密码",
|
||||||
|
"module", "SYSTEM",
|
||||||
|
"username", defaultAdminUsername,
|
||||||
|
"password_source", "environment",
|
||||||
|
)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,3 +39,26 @@ func TestEnsureDefaultAdminCreatesOnce(t *testing.T) {
|
|||||||
t.Fatalf("expected unauthorized for wrong password, got %v", err)
|
t.Fatalf("expected unauthorized for wrong password, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsureDefaultAdminUsesEnvPassword(t *testing.T) {
|
||||||
|
conn, err := db.Open(filepath.Join(t.TempDir(), "havline.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if err := db.Migrate(context.Background(), conn, filepath.Join("..", "..", "migrations")); err != nil {
|
||||||
|
t.Fatalf("migrate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("HAVLINE_INITIAL_ADMIN_PASSWORD", "env-password-123")
|
||||||
|
svc := New(conn)
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||||
|
|
||||||
|
if err := svc.EnsureDefaultAdmin(context.Background(), logger); err != nil {
|
||||||
|
t.Fatalf("bootstrap with env password: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.Login(context.Background(), "admin", "env-password-123"); err != nil {
|
||||||
|
t.Fatalf("expected env password login to succeed, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user