From ac56394dcec6cb85f1155d7551f31235771d8038 Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Thu, 1 Oct 2026 18:49:08 +0800 Subject: [PATCH] feat(fnos): scaffold Havline fnOS application package --- .gitignore | 3 + docs/fnos-app-packaging-plan.md | 27 +++++++ fnos/README.md | 76 ++++++++++++++++++++ fnos/build.ps1 | 41 +++++++++++ fnos/havline/ICON.PNG | Bin 0 -> 1134 bytes fnos/havline/ICON_256.PNG | Bin 0 -> 4429 bytes fnos/havline/app/docker/docker-compose.yaml | 21 ++++++ fnos/havline/app/ui/config | 16 +++++ fnos/havline/app/ui/images/icon_256.png | Bin 0 -> 4429 bytes fnos/havline/app/ui/images/icon_64.png | Bin 0 -> 1134 bytes fnos/havline/cmd/config_callback | 12 ++++ fnos/havline/cmd/config_init | 2 + fnos/havline/cmd/install_callback | 8 +++ fnos/havline/cmd/install_init | 2 + fnos/havline/cmd/main | 29 ++++++++ fnos/havline/cmd/uninstall_callback | 8 +++ fnos/havline/cmd/uninstall_init | 2 + fnos/havline/cmd/upgrade_callback | 14 ++++ fnos/havline/cmd/upgrade_init | 2 + fnos/havline/config/privilege | 7 ++ fnos/havline/config/resource | 17 +++++ fnos/havline/manifest | 14 ++++ fnos/havline/wizard/config | 42 +++++++++++ fnos/havline/wizard/install | 70 ++++++++++++++++++ fnos/havline/wizard/uninstall | 21 ++++++ internal/auth/auth.go | 35 ++++++--- internal/auth/auth_test.go | 23 ++++++ 27 files changed, 483 insertions(+), 9 deletions(-) create mode 100644 fnos/README.md create mode 100644 fnos/build.ps1 create mode 100644 fnos/havline/ICON.PNG create mode 100644 fnos/havline/ICON_256.PNG create mode 100644 fnos/havline/app/docker/docker-compose.yaml create mode 100644 fnos/havline/app/ui/config create mode 100644 fnos/havline/app/ui/images/icon_256.png create mode 100644 fnos/havline/app/ui/images/icon_64.png create mode 100644 fnos/havline/cmd/config_callback create mode 100644 fnos/havline/cmd/config_init create mode 100644 fnos/havline/cmd/install_callback create mode 100644 fnos/havline/cmd/install_init create mode 100644 fnos/havline/cmd/main create mode 100644 fnos/havline/cmd/uninstall_callback create mode 100644 fnos/havline/cmd/uninstall_init create mode 100644 fnos/havline/cmd/upgrade_callback create mode 100644 fnos/havline/cmd/upgrade_init create mode 100644 fnos/havline/config/privilege create mode 100644 fnos/havline/config/resource create mode 100644 fnos/havline/manifest create mode 100644 fnos/havline/wizard/config create mode 100644 fnos/havline/wizard/install create mode 100644 fnos/havline/wizard/uninstall diff --git a/.gitignore b/.gitignore index 1e225d7..0fd790c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ dist/ output/ web/public/111favicon.png web/src/assets/brand/logo00.png +fnos/*.fpk +fnos/.build/ +fnos/dist/ # scripts/ 下的本地辅助脚本不入库,但随仓库分发的 agent 安装脚本需要跟踪 scripts/* !scripts/agent-install.sh diff --git a/docs/fnos-app-packaging-plan.md b/docs/fnos-app-packaging-plan.md index 6c663c5..76e22bb 100644 --- a/docs/fnos-app-packaging-plan.md +++ b/docs/fnos-app-packaging-plan.md @@ -1001,6 +1001,25 @@ Havline 适合做飞牛 Docker 应用,不适合首发做成 CGI 或原生应 首发使用飞牛应用中心升级。`manifest.changelog` 应面向用户简洁表达本次变更; 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 建议的首发实施顺序 阶段 1 只做“能安装、能打开、能保留数据”: @@ -1014,6 +1033,14 @@ Havline 适合做飞牛 Docker 应用,不适合首发做成 CGI 或原生应 7. 构建 amd64 / arm64 镜像。 8. 用 `fnpack build` 打包并在飞牛测试设备安装验证。 +当前已经在仓库内生成: + +```text +fnos/havline/ +``` + +并补上了首发骨架所需的基础配置。 + 阶段 1 暂不实现: - Havline 自带 updater; diff --git a/fnos/README.md b/fnos/README.md new file mode 100644 index 0000000..839bf6a --- /dev/null +++ b/fnos/README.md @@ -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-.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 目录。 +- 应用中心负责安装、升级、启动、停止和卸载。 diff --git a/fnos/build.ps1 b/fnos/build.ps1 new file mode 100644 index 0000000..97ec252 --- /dev/null +++ b/fnos/build.ps1 @@ -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 +} diff --git a/fnos/havline/ICON.PNG b/fnos/havline/ICON.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c221ba643cded70c4cc1dff1f9de38d8a5512bbb GIT binary patch literal 1134 zcmV-!1d;oRP){ zB3LL&KwDGT*ceT15!(o2u(7y8P@51@3n7Icu`s2%LKFnaVWUlMGymP&%ii85x3jk& zbG!LLxUUU2??3Ooc{6)YAS4@`R!Kcf$_BpbmkWu&~y!@E!wrF+3#d zVg6BcyFeLURz}~pfEzH!;I1gco;D;NC?RTHt_y}UE`d3#fm!r1%D{!D5cOR??}W21 z8hERomXQx?mc)3FH%R{*%J@poj3@SlZv3_?{twzbAcr&Wr^6Fk0x;&4@n4W%<6S7i zDK{syLVyVm`8S?$Rhx5P*dEYy{S6cdiWG+C@-CXOzlq{erx!?UVG_lp zMBAkl7(+p!?NZhF6%-WO&LmhyaVb-P$UD5h|IzQ1BqCD%tOWRn%vt{lEUzOnkp!Ue z)sMijA!N?TQQ*l3VD&p9p)J47p&bovIW2BS&Bi^a75F zW79=Tfc0NHu4aP!y9^$59yvDM#H=O2^0%GmCM3Whs-Ltj*ArmXvtMK)GzG8G^imxn zFN4gy3vZA)9D{c9d{J5U$m67fsCgjQL(YQA>b{ zk=ns5>puj(()+5+&49$r>|(@q6O{9j7fPZN&g`Aw%F(nTu=cwsp3 zkNDl(zZ6j*4loG@4+SUQutXN1Z4g}s;13a#h+GHcg;+cwP8d$!l6xn!Sij5w$;-g#_8_o&eAA;;!v=p#aR$ebWaJGj)bQ4aKDVS* z3j2%PpUU#acj5AYB{3xVyLq(?ETLeqsfxDm|6HCg_(UbcM<@ui)oq*EZ<%r5wR3^Q zIPzT=xJ^^79>TW0fE@~3m_j~9pz%}w-ogFG$)KxkPf&4^hr~!5nlWDs`aD$gG`4VG z2wX@1P*;X8VE_w}o2UKQ?ek85(FGxKGB54(+mr=hit$C*ZU6uP07*qoM6N<$f&?e} A4*&oF literal 0 HcmV?d00001 diff --git a/fnos/havline/ICON_256.PNG b/fnos/havline/ICON_256.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d5c1bd45fd46feafd08f17e909c2c08bbeb61b48 GIT binary patch literal 4429 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 literal 0 HcmV?d00001 diff --git a/fnos/havline/app/docker/docker-compose.yaml b/fnos/havline/app/docker/docker-compose.yaml new file mode 100644 index 0000000..ea42396 --- /dev/null +++ b/fnos/havline/app/docker/docker-compose.yaml @@ -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}" diff --git a/fnos/havline/app/ui/config b/fnos/havline/app/ui/config new file mode 100644 index 0000000..a4dadc3 --- /dev/null +++ b/fnos/havline/app/ui/config @@ -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" + } + } + } +} diff --git a/fnos/havline/app/ui/images/icon_256.png b/fnos/havline/app/ui/images/icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c1bd45fd46feafd08f17e909c2c08bbeb61b48 GIT binary patch literal 4429 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 literal 0 HcmV?d00001 diff --git a/fnos/havline/app/ui/images/icon_64.png b/fnos/havline/app/ui/images/icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..c221ba643cded70c4cc1dff1f9de38d8a5512bbb GIT binary patch literal 1134 zcmV-!1d;oRP){ zB3LL&KwDGT*ceT15!(o2u(7y8P@51@3n7Icu`s2%LKFnaVWUlMGymP&%ii85x3jk& zbG!LLxUUU2??3Ooc{6)YAS4@`R!Kcf$_BpbmkWu&~y!@E!wrF+3#d zVg6BcyFeLURz}~pfEzH!;I1gco;D;NC?RTHt_y}UE`d3#fm!r1%D{!D5cOR??}W21 z8hERomXQx?mc)3FH%R{*%J@poj3@SlZv3_?{twzbAcr&Wr^6Fk0x;&4@n4W%<6S7i zDK{syLVyVm`8S?$Rhx5P*dEYy{S6cdiWG+C@-CXOzlq{erx!?UVG_lp zMBAkl7(+p!?NZhF6%-WO&LmhyaVb-P$UD5h|IzQ1BqCD%tOWRn%vt{lEUzOnkp!Ue z)sMijA!N?TQQ*l3VD&p9p)J47p&bovIW2BS&Bi^a75F zW79=Tfc0NHu4aP!y9^$59yvDM#H=O2^0%GmCM3Whs-Ltj*ArmXvtMK)GzG8G^imxn zFN4gy3vZA)9D{c9d{J5U$m67fsCgjQL(YQA>b{ zk=ns5>puj(()+5+&49$r>|(@q6O{9j7fPZN&g`Aw%F(nTu=cwsp3 zkNDl(zZ6j*4loG@4+SUQutXN1Z4g}s;13a#h+GHcg;+cwP8d$!l6xn!Sij5w$;-g#_8_o&eAA;;!v=p#aR$ebWaJGj)bQ4aKDVS* z3j2%PpUU#acj5AYB{3xVyLq(?ETLeqsfxDm|6HCg_(UbcM<@ui)oq*EZ<%r5wR3^Q zIPzT=xJ^^79>TW0fE@~3m_j~9pz%}w-ogFG$)KxkPf&4^hr~!5nlWDs`aD$gG`4VG z2wX@1P*;X8VE_w}o2UKQ?ek85(FGxKGB54(+mr=hit$C*ZU6uP07*qoM6N<$f&?e} A4*&oF literal 0 HcmV?d00001 diff --git a/fnos/havline/cmd/config_callback b/fnos/havline/cmd/config_callback new file mode 100644 index 0000000..a87b239 --- /dev/null +++ b/fnos/havline/cmd/config_callback @@ -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 diff --git a/fnos/havline/cmd/config_init b/fnos/havline/cmd/config_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/fnos/havline/cmd/config_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos/havline/cmd/install_callback b/fnos/havline/cmd/install_callback new file mode 100644 index 0000000..e380a62 --- /dev/null +++ b/fnos/havline/cmd/install_callback @@ -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 diff --git a/fnos/havline/cmd/install_init b/fnos/havline/cmd/install_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/fnos/havline/cmd/install_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos/havline/cmd/main b/fnos/havline/cmd/main new file mode 100644 index 0000000..40a9506 --- /dev/null +++ b/fnos/havline/cmd/main @@ -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 diff --git a/fnos/havline/cmd/uninstall_callback b/fnos/havline/cmd/uninstall_callback new file mode 100644 index 0000000..7c99014 --- /dev/null +++ b/fnos/havline/cmd/uninstall_callback @@ -0,0 +1,8 @@ +#!/bin/bash +set -u + +if [ "${wizard_uninstall_mode:-keep}" = "purge" ]; then + rm -rf "$TRIM_PKGVAR" +fi + +exit 0 diff --git a/fnos/havline/cmd/uninstall_init b/fnos/havline/cmd/uninstall_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/fnos/havline/cmd/uninstall_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos/havline/cmd/upgrade_callback b/fnos/havline/cmd/upgrade_callback new file mode 100644 index 0000000..c22a518 --- /dev/null +++ b/fnos/havline/cmd/upgrade_callback @@ -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 diff --git a/fnos/havline/cmd/upgrade_init b/fnos/havline/cmd/upgrade_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/fnos/havline/cmd/upgrade_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/fnos/havline/config/privilege b/fnos/havline/config/privilege new file mode 100644 index 0000000..cff6a3a --- /dev/null +++ b/fnos/havline/config/privilege @@ -0,0 +1,7 @@ +{ + "defaults": { + "run-as": "package" + }, + "username": "havline", + "groupname": "havline" +} diff --git a/fnos/havline/config/resource b/fnos/havline/config/resource new file mode 100644 index 0000000..633747b --- /dev/null +++ b/fnos/havline/config/resource @@ -0,0 +1,17 @@ +{ + "docker-project": { + "projects": [ + { + "name": "havline", + "path": "docker" + } + ] + }, + "data-share": { + "shares": [ + { + "name": "havline/backups" + } + ] + } +} diff --git a/fnos/havline/manifest b/fnos/havline/manifest new file mode 100644 index 0000000..ad86aa8 --- /dev/null +++ b/fnos/havline/manifest @@ -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 = 首发飞牛应用:支持安装向导、端口配置、数据持久化和应用中心升级 diff --git a/fnos/havline/wizard/config b/fnos/havline/wizard/config new file mode 100644 index 0000000..dd0a823 --- /dev/null +++ b/fnos/havline/wizard/config @@ -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" } + ] + } + ] + } +] diff --git a/fnos/havline/wizard/install b/fnos/havline/wizard/install new file mode 100644 index 0000000..f187338 --- /dev/null +++ b/fnos/havline/wizard/install @@ -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" } + ] + } + ] + } +] diff --git a/fnos/havline/wizard/uninstall b/fnos/havline/wizard/uninstall new file mode 100644 index 0000000..3545dd5 --- /dev/null +++ b/fnos/havline/wizard/uninstall @@ -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 配置,且无法恢复。" + } + ] + } +] diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 160b9ab..78c5153 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -8,6 +8,8 @@ import ( "errors" "fmt" "log/slog" + "os" + "strings" "time" "golang.org/x/crypto/bcrypt" @@ -45,20 +47,35 @@ func (s *Service) EnsureDefaultAdmin(ctx context.Context, log *slog.Logger) erro return nil } - password, err := generatePassword(16) - if err != nil { - return err + password := strings.TrimSpace(os.Getenv("HAVLINE_INITIAL_ADMIN_PASSWORD")) + generated := false + 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 { return err } - log.Info( - "首次启动已创建管理员账户,请使用以下凭据登录,并在「设置」中尽快修改密码", - "module", "SYSTEM", - "username", defaultAdminUsername, - "password", password, - ) + if generated { + log.Info( + "首次启动已创建管理员账户,请使用以下凭据登录,并在「设置」中尽快修改密码", + "module", "SYSTEM", + "username", defaultAdminUsername, + "password", password, + ) + } else { + log.Info( + "首次启动已使用 HAVLINE_INITIAL_ADMIN_PASSWORD 创建管理员账户,请在登录后尽快修改密码", + "module", "SYSTEM", + "username", defaultAdminUsername, + "password_source", "environment", + ) + } return nil } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 776fc2a..c58a80f 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -39,3 +39,26 @@ func TestEnsureDefaultAdminCreatesOnce(t *testing.T) { 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) + } +}