mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
docs: add guided panel deployment
This commit is contained in:
15
.env.example
15
.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"
|
DATABASE_URL="postgresql://jboard:jboard123@localhost:5432/jboard"
|
||||||
|
|
||||||
# NextAuth
|
# 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_SECRET="replace-with-a-long-random-secret"
|
||||||
NEXTAUTH_URL="https://your-domain.com"
|
NEXTAUTH_URL="https://your-domain.com"
|
||||||
|
|
||||||
# Must be at least 32 bytes, used for AES-256-GCM encryption
|
# Must be at least 32 bytes, used for AES-256-GCM encryption
|
||||||
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
|
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"
|
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"
|
||||||
|
|||||||
129
README.md
129
README.md
@@ -94,15 +94,21 @@ J-Board 只保存售卖和展示所需的节点、入站、客户端镜像数据
|
|||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
以 `.env.example` 为准,运行至少需要:
|
以 `.env.example` 为准。生产部署推荐直接使用一键脚本生成 `.env`,手动配置时请重点确认这些值:
|
||||||
|
|
||||||
- `DATABASE_URL`
|
| 变量 | 用途 | 说明 |
|
||||||
- `NEXTAUTH_SECRET`
|
| --- | --- | --- |
|
||||||
- `NEXTAUTH_URL`
|
| `APP_PORT` | 面板监听端口 | 默认 `3000`。反向代理应转发到 `http://127.0.0.1:APP_PORT`。 |
|
||||||
- `ENCRYPTION_KEY`
|
| `SITE_NAME` | 站点名称 | 初始化系统设置和邮件模板会使用。 |
|
||||||
- `REDIS_URL`
|
| `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,不提交迁移脚本
|
- 不维护 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
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# 编辑 .env,尤其是 NEXTAUTH_URL、NEXTAUTH_SECRET、ENCRYPTION_KEY、POSTGRES_PASSWORD、管理员账号
|
||||||
docker compose build init app
|
docker compose build init app
|
||||||
docker compose --profile setup run --rm init
|
docker compose --profile setup run --rm init
|
||||||
docker compose up -d app
|
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
|
docker compose up -d app
|
||||||
```
|
```
|
||||||
|
|
||||||
|
仓库内也提供更新脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/upgrade-jboard-panel.sh
|
||||||
|
```
|
||||||
|
|
||||||
常用排障:
|
常用排障:
|
||||||
|
|
||||||
- 查看状态:`docker compose ps`
|
- 查看状态:`docker compose ps`
|
||||||
- 查看日志:`docker compose logs -f app`
|
- 查看日志:`docker compose logs -f app`
|
||||||
- 页面仍是旧版本:确认已执行 `docker compose build init app` 和 `docker compose up -d 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'`
|
- 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 密码、支付密钥
|
- 不要提交 `.env`、探测 Token、3x-ui 密码、支付密钥
|
||||||
|
- 数据库备份里包含用户、订单和支付配置,建议加密保存并限制下载权限
|
||||||
- 生产环境不要公开 PostgreSQL 和 Redis 端口
|
- 生产环境不要公开 PostgreSQL 和 Redis 端口
|
||||||
- 3x-ui 面板建议限制来源 IP 或使用反向代理鉴权
|
- 3x-ui 面板建议限制来源 IP 或使用反向代理鉴权
|
||||||
- `ENCRYPTION_KEY` 一旦生产使用不要随意更换,否则已加密数据会无法解密
|
- `ENCRYPTION_KEY` 一旦生产使用不要随意更换,否则已加密数据会无法解密
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ services:
|
|||||||
target: runner
|
target: runner
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "${APP_PORT:-3000}:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://jboard:jboard123@db:5432/jboard
|
- DATABASE_URL=postgresql://jboard:${POSTGRES_PASSWORD:-jboard123}@db:5432/jboard
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/public/app-info || exit 1"]
|
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
|
condition: service_healthy
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://jboard:jboard123@db:5432/jboard
|
- DATABASE_URL=postgresql://jboard:${POSTGRES_PASSWORD:-jboard123}@db:5432/jboard
|
||||||
profiles:
|
profiles:
|
||||||
- setup
|
- setup
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: jboard
|
POSTGRES_DB: jboard
|
||||||
POSTGRES_USER: jboard
|
POSTGRES_USER: jboard
|
||||||
POSTGRES_PASSWORD: jboard123
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-jboard123}
|
||||||
# 仅暴露给内部网络,生产环境不要对外开放
|
# 仅暴露给内部网络,生产环境不要对外开放
|
||||||
# ports:
|
# ports:
|
||||||
# - "5432:5432"
|
# - "5432:5432"
|
||||||
|
|||||||
@@ -6,21 +6,48 @@ import bcrypt from "bcryptjs";
|
|||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
const prisma = new PrismaClient({ adapter });
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
async function main() {
|
function envValue(key: string, fallback: string) {
|
||||||
const hashedPassword = await bcrypt.hash("admin123", 12);
|
const value = process.env[key]?.trim();
|
||||||
|
return value || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.user.upsert({
|
async function main() {
|
||||||
where: { email: "admin@jboard.local" },
|
const adminEmail = envValue("ADMIN_EMAIL", "admin@jboard.local").toLowerCase();
|
||||||
update: {},
|
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: {
|
create: {
|
||||||
email: "admin@jboard.local",
|
id: "default",
|
||||||
password: hashedPassword,
|
siteName,
|
||||||
name: "Admin",
|
siteUrl,
|
||||||
role: "ADMIN",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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()
|
main()
|
||||||
|
|||||||
434
scripts/install-jboard-panel.sh
Executable file
434
scripts/install-jboard-panel.sh
Executable file
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user