diff --git a/.env.example b/.env.example index 03b1186..1f722cd 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,23 @@ -# PostgreSQL +# J-Board panel +APP_PORT="3000" +SITE_NAME="J-Board" + +# PostgreSQL for local tools; Docker Compose overrides host to db +POSTGRES_PASSWORD="jboard123" DATABASE_URL="postgresql://jboard:jboard123@localhost:5432/jboard" # NextAuth +# Use the public domain you will reverse proxy to this panel, for example https://panel.example.com NEXTAUTH_SECRET="replace-with-a-long-random-secret" NEXTAUTH_URL="https://your-domain.com" # Must be at least 32 bytes, used for AES-256-GCM encryption ENCRYPTION_KEY="0123456789abcdef0123456789abcdef" -# Redis connection URL for rate limiting and caching +# Redis connection URL for local tools; Docker Compose overrides host to redis REDIS_URL="redis://localhost:6379" + +# Initial admin account, used by npm run db:seed on first install +ADMIN_EMAIL="admin@jboard.local" +ADMIN_PASSWORD="admin123" +ADMIN_NAME="Admin" diff --git a/README.md b/README.md index 755d41f..600288c 100644 --- a/README.md +++ b/README.md @@ -94,15 +94,21 @@ J-Board 只保存售卖和展示所需的节点、入站、客户端镜像数据 ## 环境变量 -以 `.env.example` 为准,运行至少需要: +以 `.env.example` 为准。生产部署推荐直接使用一键脚本生成 `.env`,手动配置时请重点确认这些值: -- `DATABASE_URL` -- `NEXTAUTH_SECRET` -- `NEXTAUTH_URL` -- `ENCRYPTION_KEY` -- `REDIS_URL` +| 变量 | 用途 | 说明 | +| --- | --- | --- | +| `APP_PORT` | 面板监听端口 | 默认 `3000`。反向代理应转发到 `http://127.0.0.1:APP_PORT`。 | +| `SITE_NAME` | 站点名称 | 初始化系统设置和邮件模板会使用。 | +| `NEXTAUTH_URL` | 公网访问地址 | 必须填写你准备反代到面板的正式域名,例如 `https://panel.example.com`。不要填容器内地址。 | +| `NEXTAUTH_SECRET` | 登录会话密钥 | 生产环境必须使用随机长字符串。 | +| `ENCRYPTION_KEY` | 敏感信息加密密钥 | 至少 32 字节。生产使用后不要更换,否则 3x-ui 密码、探测 Token、流媒体凭据等已加密数据会无法解密。 | +| `DATABASE_URL` | PostgreSQL 连接 | 本地工具使用;Docker 部署时 Compose 会覆盖为容器内数据库地址。 | +| `POSTGRES_PASSWORD` | Docker PostgreSQL 密码 | 一键脚本会自动生成。 | +| `REDIS_URL` | Redis 连接 | 本地工具使用;Docker 部署时 Compose 会覆盖为容器内 Redis 地址。 | +| `ADMIN_EMAIL` / `ADMIN_PASSWORD` / `ADMIN_NAME` | 初始管理员 | 首次 `db:seed` 创建管理员账号。已有数据库不会强制重置旧管理员密码。 | -`ENCRYPTION_KEY` 必须至少 32 字节,用于 3x-ui 面板密码、探测 Token、流媒体凭据等敏感信息加密。Docker 部署时 `DATABASE_URL` 与 `REDIS_URL` 会由 `docker-compose.yml` 覆盖为容器内地址。 +SMTP 邮件服务、注册邮箱验证开关、支付方式、3x-ui 节点等业务配置都在管理后台填写,不建议写进 `.env`。 ## 本地开发 @@ -135,11 +141,75 @@ npm run build - 不维护 Prisma migrations,不提交迁移脚本 - 删除字段或模型时同步清理引用、文档和导出逻辑 -## Docker 部署 +## 部署 + +### 一键部署(推荐) + +适合全新的 Linux 服务器。脚本会自动安装 Docker 与 Compose 插件,拉取代码,询问并生成 `.env`,初始化数据库,启动面板,最后输出访问地址、反代目标和管理员账号。 + +```bash +curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/main/scripts/install-jboard-panel.sh | bash +``` + +脚本会询问这些信息;直接回车即可使用默认值或自动生成值: + +| 提示项 | 含义 | +| --- | --- | +| 安装目录 | 默认 `/opt/jboard`;如果在仓库内运行脚本则默认当前仓库。 | +| 站点名称 | 面板标题、邮件模板和初始化系统设置会使用。 | +| 公网访问地址 | 你准备反向代理到本机 `3000` 端口的域名,例如 `https://panel.example.com`。没有域名时可先用 `http://服务器IP:3000` 测试。 | +| 本机监听端口 | 默认 `3000`,Nginx/Caddy/宝塔反代目标就是 `http://127.0.0.1:3000`。 | +| 管理员邮箱和密码 | 首次初始化会创建该管理员,脚本完成后会再次打印。 | +| PostgreSQL 密码、`NEXTAUTH_SECRET`、`ENCRYPTION_KEY` | 可手动输入;回车会自动生成安全值。 | + +也可以用环境变量覆盖默认行为: + +```bash +APP_DIR=/opt/jboard GH_REPO=JetSprow/J-Board BRANCH=main bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/main/scripts/install-jboard-panel.sh) +``` + +脚本完成后,你会看到类似信息: + +```text +访问地址:https://panel.example.com +反代目标:http://127.0.0.1:3000 +管理员邮箱:admin@example.com +管理员密码:自动生成或你输入的密码 +``` + +### 反向代理 + +`NEXTAUTH_URL` 和后台“系统设置 -> 站点域名 / URL”都应该填写公网域名,也就是你准备给用户访问、并反向代理到 J-Board 的域名。不要填写 `localhost`、容器名或内网地址。 + +Nginx 示例: + +```nginx +server { + listen 80; + server_name panel.example.com; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +正式上线建议再用 Certbot、宝塔、1Panel、Caddy 或 CDN 申请 HTTPS 证书,然后把 `NEXTAUTH_URL` 改为 `https://panel.example.com`。 + +### 手动 Docker 部署 首次启动: ```bash +cp .env.example .env +# 编辑 .env,尤其是 NEXTAUTH_URL、NEXTAUTH_SECRET、ENCRYPTION_KEY、POSTGRES_PASSWORD、管理员账号 docker compose build init app docker compose --profile setup run --rm init docker compose up -d app @@ -154,12 +224,54 @@ docker compose --profile setup run --rm init sh -lc 'npm run db:push' docker compose up -d app ``` +仓库内也提供更新脚本: + +```bash +./scripts/upgrade-jboard-panel.sh +``` + 常用排障: - 查看状态:`docker compose ps` - 查看日志:`docker compose logs -f app` - 页面仍是旧版本:确认已执行 `docker compose build init app` 和 `docker compose up -d app` - Schema 没有生效:单独运行 `docker compose --profile setup run --rm init sh -lc 'npm run db:push'` +- 登录回调、邮件链接或支付回跳出现 `localhost`:检查 `.env` 里的 `NEXTAUTH_URL` 和后台系统设置里的站点域名。 + +### 部署后检查清单 + +1. 登录 `/admin`,进入“系统设置”,确认站点域名就是你的反代域名。 +2. 配置 SMTP 邮件服务并点击“测试”,再按需要开启注册邮箱验证。 +3. 进入“支付配置”,填写并启用至少一种支付方式。 +4. 添加 3x-ui 节点,测试连接并同步入站。 +5. 创建套餐,绑定入站或流媒体服务。 +6. 用普通用户注册、下单、支付、查看订阅,走一遍完整流程。 + +可以展示给用户的常用入口: + +- 登录:`https://你的域名/login` +- 注册:`https://你的域名/register` +- 套餐商店:`https://你的域名/store` +- 用户中心:`https://你的域名/dashboard` +- 订阅列表:`https://你的域名/subscriptions` + +## 支付配置 + +支付配置在后台 `/admin/payments` 完成,密钥会保存在数据库中,不写入 `.env`,也不要提交到仓库或截图外传。创建订单时,系统会根据用户选择的支付方式生成支付链接、二维码或链上收款信息;支付成功后进入 `src/services/payment/process.ts` 完成订单确认和订阅开通。 + +| 支付方式 | 适用场景 | 必填信息 | 回调 / 查询说明 | +| --- | --- | --- | --- | +| 易支付 | 第三方聚合支付,常用于支付宝/微信通道 | API 地址、商户 ID、商户密钥、启用渠道 | 通知地址为 `https://你的域名/api/payment/notify/epay`。支持 `alipay`、`wxpay` 渠道。 | +| 支付宝当面付 | 支付宝官方扫码支付 | App ID、应用私钥、支付宝公钥、网关地址 | 通知地址为 `https://你的域名/api/payment/notify/alipay_f2f`,也支持订单查询兜底。 | +| USDT TRC20 | 加密货币收款 | TRC20 钱包地址、汇率,可选 TronGrid API Key | 没有传统回调,系统按订单金额查询近期 TRC20 入账。建议配置 TronGrid API Key 提高稳定性。 | + +支付上线前建议: + +- 在支付平台后台把通知域名、回跳域名、应用网关白名单都设置为你的公网域名。 +- 先创建低金额测试套餐,确认“创建订单 -> 支付 -> 回调/查询 -> 自动开通订阅”完整可用。 +- 易支付的 API 地址不要带尾部路径,例如填 `https://pay.example.com`,系统会自动请求 `/mapi.php`、`/submit.php` 和 `/api.php`。 +- 支付宝密钥可以填写纯 key 内容或 PEM 格式;系统会自动补 PEM 包装。 +- USDT TRC20 按金额匹配入账,测试时避免短时间出现多笔完全相同金额。 ## 节点与探测 @@ -197,6 +309,7 @@ docker compose exec -T db pg_dump -U jboard jboard > backup_$(date +%Y%m%d_%H%M% 安全建议: - 不要提交 `.env`、探测 Token、3x-ui 密码、支付密钥 +- 数据库备份里包含用户、订单和支付配置,建议加密保存并限制下载权限 - 生产环境不要公开 PostgreSQL 和 Redis 端口 - 3x-ui 面板建议限制来源 IP 或使用反向代理鉴权 - `ENCRYPTION_KEY` 一旦生产使用不要随意更换,否则已加密数据会无法解密 diff --git a/docker-compose.yml b/docker-compose.yml index 9f8f903..1125aff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: target: runner restart: unless-stopped ports: - - "3000:3000" + - "${APP_PORT:-3000}:3000" depends_on: db: condition: service_healthy @@ -13,7 +13,7 @@ services: condition: service_healthy env_file: .env environment: - - DATABASE_URL=postgresql://jboard:jboard123@db:5432/jboard + - DATABASE_URL=postgresql://jboard:${POSTGRES_PASSWORD:-jboard123}@db:5432/jboard - REDIS_URL=redis://redis:6379 healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/public/app-info || exit 1"] @@ -33,7 +33,7 @@ services: condition: service_healthy env_file: .env environment: - - DATABASE_URL=postgresql://jboard:jboard123@db:5432/jboard + - DATABASE_URL=postgresql://jboard:${POSTGRES_PASSWORD:-jboard123}@db:5432/jboard profiles: - setup @@ -45,7 +45,7 @@ services: environment: POSTGRES_DB: jboard POSTGRES_USER: jboard - POSTGRES_PASSWORD: jboard123 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-jboard123} # 仅暴露给内部网络,生产环境不要对外开放 # ports: # - "5432:5432" diff --git a/prisma/seed.ts b/prisma/seed.ts index 4b4b007..4f789c2 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -6,21 +6,48 @@ import bcrypt from "bcryptjs"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); -async function main() { - const hashedPassword = await bcrypt.hash("admin123", 12); +function envValue(key: string, fallback: string) { + const value = process.env[key]?.trim(); + return value || fallback; +} - await prisma.user.upsert({ - where: { email: "admin@jboard.local" }, - update: {}, +async function main() { + const adminEmail = envValue("ADMIN_EMAIL", "admin@jboard.local").toLowerCase(); + const adminPassword = process.env.ADMIN_PASSWORD || "admin123"; + const adminName = envValue("ADMIN_NAME", "Admin"); + const siteName = envValue("SITE_NAME", "J-Board"); + const siteUrl = process.env.NEXTAUTH_URL?.trim() || null; + const hashedPassword = await bcrypt.hash(adminPassword, 12); + + await prisma.appConfig.upsert({ + where: { id: "default" }, + update: { + siteName, + siteUrl, + }, create: { - email: "admin@jboard.local", - password: hashedPassword, - name: "Admin", - role: "ADMIN", + id: "default", + siteName, + siteUrl, }, }); - console.log("Seed completed: admin@jboard.local / admin123"); + await prisma.user.upsert({ + where: { email: adminEmail }, + update: { + name: adminName, + role: "ADMIN", + }, + create: { + email: adminEmail, + password: hashedPassword, + name: adminName, + role: "ADMIN", + emailVerifiedAt: new Date(), + }, + }); + + console.log(`Seed completed: admin ${adminEmail}`); } main() diff --git a/scripts/install-jboard-panel.sh b/scripts/install-jboard-panel.sh new file mode 100755 index 0000000..4b81f1e --- /dev/null +++ b/scripts/install-jboard-panel.sh @@ -0,0 +1,434 @@ +#!/usr/bin/env bash +set -euo pipefail + +GH_REPO="${GH_REPO:-JetSprow/J-Board}" +BRANCH="${BRANCH:-main}" +APP_DIR="${APP_DIR:-}" +REWRITE_ENV="${REWRITE_ENV:-}" +SKIP_DOCKER_INSTALL="${SKIP_DOCKER_INSTALL:-0}" + +APP_PORT="" +PUBLIC_URL="" +SITE_NAME="" +ADMIN_EMAIL="" +ADMIN_PASSWORD="" +ADMIN_NAME="" +POSTGRES_PASSWORD="" +NEXTAUTH_SECRET="" +ENCRYPTION_KEY="" +ENV_REUSED="0" + +is_interactive() { + [ -r /dev/tty ] && [ -w /dev/tty ] +} + +prompt_print() { + if is_interactive; then + printf '%b' "$*" > /dev/tty + else + printf '%b' "$*" >&2 + fi +} + +prompt_read() { + local __var="$1" + if is_interactive; then + IFS= read -r "$__var" < /dev/tty || true + else + return 1 + fi +} + +run_as_root() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + elif command -v sudo >/dev/null 2>&1; then + sudo "$@" + else + echo "需要 root 权限。请使用 root 用户运行,或先安装 sudo。" >&2 + exit 1 + fi +} + +line() { + printf '%s\n' "------------------------------------------------------------" +} + +section() { + echo + line + printf '%s\n' "$1" + line +} + +need_command() { + command -v "$1" >/dev/null 2>&1 +} + +install_base_packages() { + local missing=() + for cmd in curl git openssl; do + if ! need_command "$cmd"; then + missing+=("$cmd") + fi + done + + if [ "${#missing[@]}" -eq 0 ]; then + return + fi + + section "安装基础依赖:${missing[*]}" + if need_command apt-get; then + run_as_root apt-get update + run_as_root apt-get install -y ca-certificates curl git openssl + elif need_command dnf; then + run_as_root dnf install -y ca-certificates curl git openssl + elif need_command yum; then + run_as_root yum install -y ca-certificates curl git openssl + elif need_command apk; then + run_as_root apk add --no-cache ca-certificates curl git openssl + else + echo "无法识别包管理器,请先手动安装:curl git openssl" >&2 + exit 1 + fi +} + +install_docker() { + if [ "$SKIP_DOCKER_INSTALL" = "1" ]; then + return + fi + + if need_command docker && run_as_root docker compose version >/dev/null 2>&1; then + return + fi + + section "安装 Docker 与 Compose 插件" + curl -fsSL https://get.docker.com -o /tmp/get-docker.sh + run_as_root sh /tmp/get-docker.sh + rm -f /tmp/get-docker.sh + + if need_command systemctl; then + run_as_root systemctl enable --now docker || true + elif need_command service; then + run_as_root service docker start || true + fi + + if ! run_as_root docker compose version >/dev/null 2>&1; then + echo "Docker Compose 插件安装后仍不可用,请检查 Docker 安装状态。" >&2 + exit 1 + fi +} + +random_hex() { + openssl rand -hex "$1" +} + +random_password() { + openssl rand -hex 12 +} + +server_ip() { + curl -fsS --max-time 3 https://api.ipify.org 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1" +} + +normalize_url() { + local value="$1" + value="${value%/}" + if [ -z "$value" ]; then + printf '%s' "$value" + return + fi + case "$value" in + http://*|https://*) printf '%s' "$value" ;; + *) printf 'https://%s' "$value" ;; + esac +} + +prompt_value() { + local label="$1" + local default="$2" + local help="${3:-}" + local value="" + + if [ -n "$help" ]; then + prompt_print "$help\n" + fi + + if is_interactive; then + prompt_print "$label [$default]: " + prompt_read value || true + fi + + if [ -z "$value" ]; then + value="$default" + fi + printf '%s' "$value" +} + +prompt_generated() { + local label="$1" + local default="$2" + local help="${3:-}" + local value="" + + if [ -n "$help" ]; then + prompt_print "$help\n" + fi + + if is_interactive; then + prompt_print "$label [回车自动生成]: " + prompt_read value || true + fi + + if [ -z "$value" ]; then + value="$default" + fi + printf '%s' "$value" +} + +prompt_yes_no() { + local label="$1" + local default="$2" + local value="" + + if ! is_interactive; then + printf '%s' "$default" + return + fi + + prompt_print "$label [$default]: " + prompt_read value || true + value="${value:-$default}" + case "$value" in + y|Y|yes|YES|Yes) printf 'y' ;; + *) printf 'n' ;; + esac +} + +resolve_default_app_dir() { + local source="${BASH_SOURCE[0]:-}" + local dir="" + + if [ -n "$source" ] && [ -f "$source" ]; then + dir="$(cd -- "$(dirname -- "$source")" && pwd)" + if [ -f "$dir/../package.json" ]; then + cd -- "$dir/.." && pwd + return + fi + fi + + echo "/opt/jboard" +} + +git_in_repo() { + if [ -w "$APP_DIR/.git" ] || [ -w "$APP_DIR" ]; then + git -c safe.directory="$APP_DIR" "$@" + else + run_as_root git -c safe.directory="$APP_DIR" "$@" + fi +} + +prepare_repo() { + section "准备 J-Board 代码" + + local default_dir + default_dir="$(resolve_default_app_dir)" + APP_DIR="$(prompt_value "安装目录" "${APP_DIR:-$default_dir}" "如果你已经在仓库目录里运行脚本,直接回车即可。")" + + if [ -d "$APP_DIR/.git" ]; then + echo "检测到已有仓库:$APP_DIR" + cd "$APP_DIR" + git_in_repo fetch origin "$BRANCH" + git_in_repo checkout "$BRANCH" + git_in_repo pull --ff-only origin "$BRANCH" + elif [ -e "$APP_DIR" ] && [ "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')" != "0" ]; then + echo "安装目录已存在且不为空:$APP_DIR" >&2 + echo "请换一个目录,或设置 APP_DIR 指向空目录/已有 J-Board 仓库。" >&2 + exit 1 + else + run_as_root mkdir -p "$(dirname "$APP_DIR")" + run_as_root git clone --branch "$BRANCH" "https://github.com/${GH_REPO}.git" "$APP_DIR" + cd "$APP_DIR" + fi +} + +load_existing_env() { + if [ -f .env ]; then + if [ -r .env ]; then + set -a + # shellcheck disable=SC1091 + . ./.env + set +a + else + echo ".env 存在但当前用户不可读取;如需显示管理员密码,请使用 root 重新运行或查看 .env。" + fi + APP_PORT="${APP_PORT:-3000}" + PUBLIC_URL="${NEXTAUTH_URL:-}" + SITE_NAME="${SITE_NAME:-J-Board}" + ADMIN_EMAIL="${ADMIN_EMAIL:-admin@jboard.local}" + ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" + ADMIN_NAME="${ADMIN_NAME:-Admin}" + fi +} + +env_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/\\$/g' +} + +write_env() { + local tmp + tmp="$(mktemp)" + + { + printf '# J-Board panel\n' + printf 'APP_PORT="%s"\n' "$(env_escape "$APP_PORT")" + printf 'SITE_NAME="%s"\n' "$(env_escape "$SITE_NAME")" + printf '\n# PostgreSQL for local tools; Docker Compose overrides host to db\n' + printf 'POSTGRES_PASSWORD="%s"\n' "$(env_escape "$POSTGRES_PASSWORD")" + printf 'DATABASE_URL="postgresql://jboard:%s@localhost:5432/jboard"\n' "$(env_escape "$POSTGRES_PASSWORD")" + printf '\n# NextAuth\n' + printf 'NEXTAUTH_SECRET="%s"\n' "$(env_escape "$NEXTAUTH_SECRET")" + printf 'NEXTAUTH_URL="%s"\n' "$(env_escape "$PUBLIC_URL")" + printf '\n# Must be at least 32 bytes, used for AES-256-GCM encryption\n' + printf 'ENCRYPTION_KEY="%s"\n' "$(env_escape "$ENCRYPTION_KEY")" + printf '\n# Redis connection URL for local tools; Docker Compose overrides host to redis\n' + printf 'REDIS_URL="redis://localhost:6379"\n' + printf '\n# Initial admin account, used by npm run db:seed on first install\n' + printf 'ADMIN_EMAIL="%s"\n' "$(env_escape "$ADMIN_EMAIL")" + printf 'ADMIN_PASSWORD="%s"\n' "$(env_escape "$ADMIN_PASSWORD")" + printf 'ADMIN_NAME="%s"\n' "$(env_escape "$ADMIN_NAME")" + } > "$tmp" + + if [ -f .env ]; then + run_as_root cp .env ".env.backup.$(date +%Y%m%d%H%M%S)" + fi + run_as_root install -m 0600 "$tmp" .env + rm -f "$tmp" +} + +configure_env() { + section "生成 .env 配置" + + if [ -f .env ] && [ -z "$REWRITE_ENV" ]; then + local answer + answer="$(prompt_yes_no "检测到已有 .env,是否重新生成" "n")" + if [ "$answer" = "n" ]; then + ENV_REUSED="1" + load_existing_env + echo "沿用现有 .env。" + return + fi + elif [ "${REWRITE_ENV:-0}" = "0" ] && [ -f .env ]; then + ENV_REUSED="1" + load_existing_env + echo "沿用现有 .env。" + return + fi + + local ip default_url + ip="$(server_ip)" + default_url="http://${ip}:3000" + + SITE_NAME="$(prompt_value "站点名称" "J-Board")" + PUBLIC_URL="$(prompt_value "公网访问地址" "$default_url" "这里请填写你准备反向代理到本机 3000 端口的正式域名,例如 https://panel.example.com。没有域名时可先回车用 IP:3000 测试。")" + PUBLIC_URL="$(normalize_url "$PUBLIC_URL")" + APP_PORT="$(prompt_value "本机监听端口" "3000" "反向代理目标会是 http://127.0.0.1:端口,默认 3000。")" + ADMIN_EMAIL="$(prompt_value "管理员邮箱" "admin@jboard.local")" + ADMIN_PASSWORD="$(prompt_generated "管理员密码" "$(random_password)" "回车会生成一个安全密码,部署完成后会在结果中显示一次。")" + ADMIN_NAME="$(prompt_value "管理员昵称" "Admin")" + POSTGRES_PASSWORD="$(prompt_generated "PostgreSQL 密码" "$(random_password)")" + NEXTAUTH_SECRET="$(prompt_generated "NEXTAUTH_SECRET" "$(random_hex 32)")" + ENCRYPTION_KEY="$(prompt_generated "ENCRYPTION_KEY" "$(random_hex 32)" "生产使用后不要更换 ENCRYPTION_KEY,否则已加密的面板密码、Token、凭据会无法解密。")" + + write_env + echo ".env 已写入:$APP_DIR/.env" +} + +docker_compose() { + run_as_root docker compose "$@" +} + +start_panel() { + section "构建并启动面板" + docker_compose build init app + docker_compose --profile setup run --rm init + docker_compose up -d app +} + +wait_for_app() { + section "检查启动状态" + local url="http://127.0.0.1:${APP_PORT:-3000}/api/public/app-info" + local ok="0" + + for _ in $(seq 1 30); do + if curl -fsS "$url" >/dev/null 2>&1; then + ok="1" + break + fi + sleep 2 + done + + docker_compose ps + echo + if [ "$ok" = "1" ]; then + echo "健康检查通过:$url" + else + echo "健康检查暂未通过,请查看日志:docker compose logs -f app" + fi +} + +print_summary() { + local proxy_target="http://127.0.0.1:${APP_PORT:-3000}" + local shown_password="$ADMIN_PASSWORD" + if [ "$ENV_REUSED" = "1" ] && [ -z "$shown_password" ]; then + shown_password="沿用已有数据库账号;如忘记请在数据库中重置" + fi + + echo + printf '%s\n' "============================================================" + printf '%s\n' "J-Board 部署完成" + printf '%s\n' "============================================================" + printf '访问地址:%s\n' "${PUBLIC_URL:-http://127.0.0.1:${APP_PORT:-3000}}" + printf '反代目标:%s\n' "$proxy_target" + printf '管理员邮箱:%s\n' "${ADMIN_EMAIL:-admin@jboard.local}" + printf '管理员密码:%s\n' "$shown_password" + echo + printf '%s\n' "反向代理提示" + printf ' 将你的域名解析到这台服务器,并把 Web 反向代理到 %s。\n' "$proxy_target" + printf ' NEXTAUTH_URL 与后台系统设置中的站点域名都应使用这个公网域名。\n' + echo + printf '%s\n' "可以展示给用户的入口" + printf ' 首页 / 登录:%s/login\n' "${PUBLIC_URL%/}" + printf ' 注册入口:%s/register\n' "${PUBLIC_URL%/}" + printf ' 套餐商店:%s/store\n' "${PUBLIC_URL%/}" + printf ' 用户中心:%s/dashboard\n' "${PUBLIC_URL%/}" + echo + printf '%s\n' "上线前建议完成" + printf ' 1. 后台 /admin/settings:确认站点域名、SMTP 邮件服务、注册策略。\n' + printf ' 2. 后台 /admin/payments:配置并启用支付方式。\n' + printf ' 3. 后台 /admin/nodes:添加 3x-ui 节点并同步入站。\n' + printf ' 4. 后台 /admin/plans:创建套餐并绑定入站或流媒体服务。\n' + echo + printf '%s\n' "常用命令" + printf ' cd %s\n' "$APP_DIR" + printf ' sudo docker compose logs -f app\n' + printf ' sudo docker compose ps\n' + printf ' sudo ./scripts/upgrade-jboard-panel.sh\n' + printf '%s\n' "============================================================" +} + +main() { + section "J-Board 一键部署向导" + echo "这个脚本会安装 Docker、准备配置、初始化数据库并启动面板。" + echo "适合全新 Linux 服务器;已有环境会尽量保留现有 .env。" + + install_base_packages + prepare_repo + configure_env + install_docker + start_panel + wait_for_app + print_summary +} + +main "$@"