mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: remove redis service from lite deployment
This commit is contained in:
@@ -17,9 +17,6 @@ SUBSCRIPTION_URL=""
|
|||||||
# 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 local tools; Docker Compose overrides host to redis
|
|
||||||
REDIS_URL="redis://localhost:6379"
|
|
||||||
|
|
||||||
# Optional GeoIP MMDB path. The repository includes a default City database at data/GeoLite2-City.mmdb.
|
# Optional GeoIP MMDB path. The repository includes a default City database at data/GeoLite2-City.mmdb.
|
||||||
GEOIP_MMDB_PATH="data/GeoLite2-City.mmdb"
|
GEOIP_MMDB_PATH="data/GeoLite2-City.mmdb"
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -12,8 +12,8 @@ J-Board 的定位很明确:它不是新的节点控制面,也不替代 3x-ui
|
|||||||
用户浏览器 / 客户端订阅
|
用户浏览器 / 客户端订阅
|
||||||
↓
|
↓
|
||||||
Next.js App Router 面板
|
Next.js App Router 面板
|
||||||
├─ PostgreSQL:用户、订单、套餐、订阅、审计、风控事件
|
├─ SQLite:用户、订单、套餐、订阅、审计、风控事件
|
||||||
├─ Redis:限流、后台任务与缓存辅助
|
├─ 进程内限流:无需 Redis 单独容器
|
||||||
├─ 3x-ui API:同步入站、开通/暂停/删除代理客户端
|
├─ 3x-ui API:同步入站、开通/暂停/删除代理客户端
|
||||||
└─ Agent API:接收 jboard-agent 上报的延迟、路由、节点真实连接日志
|
└─ Agent API:接收 jboard-agent 上报的延迟、路由、节点真实连接日志
|
||||||
|
|
||||||
@@ -73,9 +73,8 @@ J-Board 面板和 Agent 使用相对独立的版本节奏。
|
|||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- Next.js 16 App Router + React 19
|
- Next.js 16 App Router + React 19
|
||||||
- Prisma 7 + PostgreSQL 16
|
- Prisma 7 + SQLite
|
||||||
- NextAuth 4 Credentials
|
- NextAuth 4 Credentials
|
||||||
- Redis 7
|
|
||||||
- Tailwind CSS 4 + Base UI + Sonner + Recharts
|
- Tailwind CSS 4 + Base UI + Sonner + Recharts
|
||||||
- Nodemailer SMTP 邮件
|
- Nodemailer SMTP 邮件
|
||||||
- MaxMind MMDB GeoIP
|
- MaxMind MMDB GeoIP
|
||||||
@@ -118,9 +117,7 @@ J-Board 面板和 Agent 使用相对独立的版本节奏。
|
|||||||
| `SUBSCRIPTION_URL` | 订阅访问地址 | 可选。用于生成客户端订阅链接,例如 `https://sub.example.com`;留空时复用 `NEXTAUTH_URL`。 |
|
| `SUBSCRIPTION_URL` | 订阅访问地址 | 可选。用于生成客户端订阅链接,例如 `https://sub.example.com`;留空时复用 `NEXTAUTH_URL`。 |
|
||||||
| `NEXTAUTH_SECRET` | 登录会话密钥 | 生产环境必须使用随机长字符串。 |
|
| `NEXTAUTH_SECRET` | 登录会话密钥 | 生产环境必须使用随机长字符串。 |
|
||||||
| `ENCRYPTION_KEY` | 敏感信息加密密钥 | 至少 32 字节。生产使用后不要更换,否则 3x-ui 密码、探测 Token、SMTP 密码、流媒体凭据等已加密数据会无法解密。 |
|
| `ENCRYPTION_KEY` | 敏感信息加密密钥 | 至少 32 字节。生产使用后不要更换,否则 3x-ui 密码、探测 Token、SMTP 密码、流媒体凭据等已加密数据会无法解密。 |
|
||||||
| `DATABASE_URL` | PostgreSQL 连接 | 本地工具使用;Docker 部署时 Compose 会覆盖为容器内数据库地址。 |
|
| `DATABASE_URL` | SQLite 文件地址 | 本地默认 `file:./storage/jboard.db`;Docker 部署时 Compose 会覆盖为容器内 `/app/storage/jboard.db`。 |
|
||||||
| `POSTGRES_PASSWORD` | Docker PostgreSQL 密码 | 一键脚本会自动生成。 |
|
|
||||||
| `REDIS_URL` | Redis 连接 | 本地工具使用;Docker 部署时 Compose 会覆盖为容器内 Redis 地址。 |
|
|
||||||
| `GEOIP_MMDB_PATH` | GeoIP 城市库 | 默认 `data/GeoLite2-City.mmdb`。可换成自己的 MaxMind City MMDB。 |
|
| `GEOIP_MMDB_PATH` | GeoIP 城市库 | 默认 `data/GeoLite2-City.mmdb`。可换成自己的 MaxMind City MMDB。 |
|
||||||
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` / `ADMIN_NAME` | 初始管理员 | 首次 `db:seed` 创建管理员账号。已有数据库不会强制重置旧管理员密码。 |
|
| `ADMIN_EMAIL` / `ADMIN_PASSWORD` / `ADMIN_NAME` | 初始管理员 | 首次 `db:seed` 创建管理员账号。已有数据库不会强制重置旧管理员密码。 |
|
||||||
|
|
||||||
@@ -144,7 +141,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/JetSprow/J-Board/main/script
|
|||||||
| 订阅访问地址 | 用于生成 Clash/V2rayN/Shadowrocket 等客户端订阅链接。可以和网站地址相同,也可以填独立订阅域名,例如 `https://sub.example.com`。 |
|
| 订阅访问地址 | 用于生成 Clash/V2rayN/Shadowrocket 等客户端订阅链接。可以和网站地址相同,也可以填独立订阅域名,例如 `https://sub.example.com`。 |
|
||||||
| 本机监听端口 | 默认 `3000`。Nginx、Caddy、宝塔或 1Panel 的反代目标就是 `http://127.0.0.1:3000`。 |
|
| 本机监听端口 | 默认 `3000`。Nginx、Caddy、宝塔或 1Panel 的反代目标就是 `http://127.0.0.1:3000`。 |
|
||||||
| 管理员邮箱和密码 | 首次初始化会创建该管理员,脚本完成后会再次打印。 |
|
| 管理员邮箱和密码 | 首次初始化会创建该管理员,脚本完成后会再次打印。 |
|
||||||
| PostgreSQL 密码、`NEXTAUTH_SECRET`、`ENCRYPTION_KEY` | 可手动输入;回车会自动生成安全值。 |
|
| `NEXTAUTH_SECRET`、`ENCRYPTION_KEY` | 可手动输入;回车会自动生成安全值。 |
|
||||||
|
|
||||||
也可以通过环境变量覆盖默认行为:
|
也可以通过环境变量覆盖默认行为:
|
||||||
|
|
||||||
@@ -170,7 +167,7 @@ APP_DIR=/opt/jboard GH_REPO=JetSprow/J-Board BRANCH=main bash <(curl -fsSL https
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# 编辑 .env,尤其是 NEXTAUTH_URL、SUBSCRIPTION_URL、NEXTAUTH_SECRET、ENCRYPTION_KEY、POSTGRES_PASSWORD、管理员账号
|
# 编辑 .env,尤其是 NEXTAUTH_URL、SUBSCRIPTION_URL、NEXTAUTH_SECRET、ENCRYPTION_KEY、管理员账号
|
||||||
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
|
||||||
@@ -444,7 +441,7 @@ shasum -a 256 jboard-agent-linux-amd64 jboard-agent-linux-arm64 > SHA256SUMS
|
|||||||
|
|
||||||
- 不要提交 `.env`、探测 Token、3x-ui 密码、SMTP 密码、支付密钥。
|
- 不要提交 `.env`、探测 Token、3x-ui 密码、SMTP 密码、支付密钥。
|
||||||
- 数据库备份包含用户、订单、支付配置、节点凭据和邮件配置,建议加密保存并限制下载权限。
|
- 数据库备份包含用户、订单、支付配置、节点凭据和邮件配置,建议加密保存并限制下载权限。
|
||||||
- 生产环境不要公开 PostgreSQL 和 Redis 端口。
|
- 生产环境不要公开 SQLite 文件、备份文件或管理接口。
|
||||||
- 3x-ui 面板建议限制来源 IP 或使用反向代理鉴权。
|
- 3x-ui 面板建议限制来源 IP 或使用反向代理鉴权。
|
||||||
- `ENCRYPTION_KEY` 一旦生产使用不要随意更换。
|
- `ENCRYPTION_KEY` 一旦生产使用不要随意更换。
|
||||||
- 管理后台账号建议使用强密码和专用邮箱。
|
- 管理后台账号建议使用强密码和专用邮箱。
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-3000}:3000"
|
- "${APP_PORT:-3000}:3000"
|
||||||
depends_on:
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
volumes:
|
||||||
- sqlite_data:/app/storage
|
- sqlite_data:/app/storage
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=file:/app/storage/jboard.db
|
- DATABASE_URL=file:/app/storage/jboard.db
|
||||||
- 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"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -36,20 +32,5 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- setup
|
- setup
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
# 仅暴露给内部网络,生产环境不要对外开放
|
|
||||||
# ports:
|
|
||||||
# - "6379:6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
sqlite_data:
|
sqlite_data:
|
||||||
redis_data:
|
|
||||||
|
|||||||
80
package-lock.json
generated
80
package-lock.json
generated
@@ -17,7 +17,6 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"ioredis": "^5.10.1",
|
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"maxmind": "^5.0.6",
|
"maxmind": "^5.0.6",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
@@ -2053,12 +2052,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ioredis/commands": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -5043,15 +5036,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cluster-key-slot": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/code-block-writer": {
|
"node_modules/code-block-writer": {
|
||||||
"version": "13.0.3",
|
"version": "13.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||||
@@ -5638,6 +5622,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
@@ -7686,30 +7671,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ioredis": {
|
|
||||||
"version": "5.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
|
|
||||||
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ioredis/commands": "1.5.1",
|
|
||||||
"cluster-key-slot": "^1.1.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"denque": "^2.1.0",
|
|
||||||
"lodash.defaults": "^4.2.0",
|
|
||||||
"lodash.isarguments": "^3.1.0",
|
|
||||||
"redis-errors": "^1.2.0",
|
|
||||||
"redis-parser": "^3.0.0",
|
|
||||||
"standard-as-callback": "^2.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.22.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/ioredis"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
@@ -8839,18 +8800,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash.defaults": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.isarguments": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -11267,27 +11216,6 @@
|
|||||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/redis-errors": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redis-parser": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"redis-errors": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
@@ -12134,12 +12062,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/standard-as-callback": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"ioredis": "^5.10.1",
|
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"maxmind": "^5.0.6",
|
"maxmind": "^5.0.6",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ SITE_NAME=""
|
|||||||
ADMIN_EMAIL=""
|
ADMIN_EMAIL=""
|
||||||
ADMIN_PASSWORD=""
|
ADMIN_PASSWORD=""
|
||||||
ADMIN_NAME=""
|
ADMIN_NAME=""
|
||||||
POSTGRES_PASSWORD=""
|
|
||||||
NEXTAUTH_SECRET=""
|
NEXTAUTH_SECRET=""
|
||||||
ENCRYPTION_KEY=""
|
ENCRYPTION_KEY=""
|
||||||
ENV_REUSED="0"
|
ENV_REUSED="0"
|
||||||
@@ -285,17 +284,14 @@ write_env() {
|
|||||||
printf '# J-Board panel\n'
|
printf '# J-Board panel\n'
|
||||||
printf 'APP_PORT="%s"\n' "$(env_escape "$APP_PORT")"
|
printf 'APP_PORT="%s"\n' "$(env_escape "$APP_PORT")"
|
||||||
printf 'SITE_NAME="%s"\n' "$(env_escape "$SITE_NAME")"
|
printf 'SITE_NAME="%s"\n' "$(env_escape "$SITE_NAME")"
|
||||||
printf '\n# PostgreSQL for local tools; Docker Compose overrides host to db\n'
|
printf '\n# SQLite for local tools and Docker\n'
|
||||||
printf 'POSTGRES_PASSWORD="%s"\n' "$(env_escape "$POSTGRES_PASSWORD")"
|
printf 'DATABASE_URL="file:./storage/jboard.db"\n'
|
||||||
printf 'DATABASE_URL="postgresql://jboard:%s@localhost:5432/jboard"\n' "$(env_escape "$POSTGRES_PASSWORD")"
|
|
||||||
printf '\n# NextAuth\n'
|
printf '\n# NextAuth\n'
|
||||||
printf 'NEXTAUTH_SECRET="%s"\n' "$(env_escape "$NEXTAUTH_SECRET")"
|
printf 'NEXTAUTH_SECRET="%s"\n' "$(env_escape "$NEXTAUTH_SECRET")"
|
||||||
printf 'NEXTAUTH_URL="%s"\n' "$(env_escape "$PUBLIC_URL")"
|
printf 'NEXTAUTH_URL="%s"\n' "$(env_escape "$PUBLIC_URL")"
|
||||||
printf 'SUBSCRIPTION_URL="%s"\n' "$(env_escape "$SUBSCRIPTION_PUBLIC_URL")"
|
printf 'SUBSCRIPTION_URL="%s"\n' "$(env_escape "$SUBSCRIPTION_PUBLIC_URL")"
|
||||||
printf '\n# Must be at least 32 bytes, used for AES-256-GCM encryption\n'
|
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 '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 '\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_EMAIL="%s"\n' "$(env_escape "$ADMIN_EMAIL")"
|
||||||
printf 'ADMIN_PASSWORD="%s"\n' "$(env_escape "$ADMIN_PASSWORD")"
|
printf 'ADMIN_PASSWORD="%s"\n' "$(env_escape "$ADMIN_PASSWORD")"
|
||||||
@@ -341,7 +337,6 @@ configure_env() {
|
|||||||
ADMIN_EMAIL="$(prompt_value "管理员邮箱" "admin@jboard.local")"
|
ADMIN_EMAIL="$(prompt_value "管理员邮箱" "admin@jboard.local")"
|
||||||
ADMIN_PASSWORD="$(prompt_generated "管理员密码" "$(random_password)" "回车会生成一个安全密码,部署完成后会在结果中显示一次。")"
|
ADMIN_PASSWORD="$(prompt_generated "管理员密码" "$(random_password)" "回车会生成一个安全密码,部署完成后会在结果中显示一次。")"
|
||||||
ADMIN_NAME="$(prompt_value "管理员昵称" "Admin")"
|
ADMIN_NAME="$(prompt_value "管理员昵称" "Admin")"
|
||||||
POSTGRES_PASSWORD="$(prompt_generated "PostgreSQL 密码" "$(random_password)")"
|
|
||||||
NEXTAUTH_SECRET="$(prompt_generated "NEXTAUTH_SECRET" "$(random_hex 32)")"
|
NEXTAUTH_SECRET="$(prompt_generated "NEXTAUTH_SECRET" "$(random_hex 32)")"
|
||||||
ENCRYPTION_KEY="$(prompt_generated "ENCRYPTION_KEY" "$(random_hex 32)" "生产使用后不要更换 ENCRYPTION_KEY,否则已加密的面板密码、Token、凭据会无法解密。")"
|
ENCRYPTION_KEY="$(prompt_generated "ENCRYPTION_KEY" "$(random_hex 32)" "生产使用后不要更换 ENCRYPTION_KEY,否则已加密的面板密码、Token、凭据会无法解密。")"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getRedis } from "./redis";
|
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
interface RateLimitResult {
|
interface RateLimitResult {
|
||||||
@@ -7,12 +6,13 @@ interface RateLimitResult {
|
|||||||
reset: number;
|
reset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const globalForRateLimit = globalThis as unknown as {
|
||||||
* Sliding window rate limiter using Redis.
|
rateLimitBuckets?: Map<string, Array<{ score: number; id: string }>>;
|
||||||
* @param key - Unique identifier (e.g. `ratelimit:payment:${userId}`)
|
};
|
||||||
* @param limit - Max requests allowed in the window
|
|
||||||
* @param windowSeconds - Time window in seconds
|
const buckets = globalForRateLimit.rateLimitBuckets ?? new Map<string, Array<{ score: number; id: string }>>();
|
||||||
*/
|
globalForRateLimit.rateLimitBuckets = buckets;
|
||||||
|
|
||||||
export async function rateLimit(
|
export async function rateLimit(
|
||||||
key: string,
|
key: string,
|
||||||
limit: number,
|
limit: number,
|
||||||
@@ -20,33 +20,22 @@ export async function rateLimit(
|
|||||||
): Promise<RateLimitResult> {
|
): Promise<RateLimitResult> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const windowMs = windowSeconds * 1000;
|
const windowMs = windowSeconds * 1000;
|
||||||
|
const cutoff = now - windowMs;
|
||||||
|
|
||||||
try {
|
for (const [bucketKey, items] of buckets) {
|
||||||
const redis = getRedis();
|
const active = items.filter((item) => item.score > cutoff);
|
||||||
if (redis.status === "wait") {
|
if (active.length > 0) buckets.set(bucketKey, active);
|
||||||
await redis.connect();
|
else buckets.delete(bucketKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pipeline = redis.pipeline();
|
const bucket = buckets.get(key) ?? [];
|
||||||
pipeline.zremrangebyscore(key, 0, now - windowMs);
|
bucket.push({ score: now, id: `${now}:${randomUUID()}` });
|
||||||
pipeline.zadd(key, now, `${now}:${randomUUID()}`);
|
buckets.set(key, bucket);
|
||||||
pipeline.zcard(key);
|
|
||||||
pipeline.expire(key, windowSeconds);
|
|
||||||
|
|
||||||
const results = await pipeline.exec();
|
|
||||||
const count = (results?.[2]?.[1] as number) ?? 0;
|
|
||||||
|
|
||||||
|
const count = bucket.length;
|
||||||
return {
|
return {
|
||||||
success: count <= limit,
|
success: count <= limit,
|
||||||
remaining: Math.max(0, limit - count),
|
remaining: Math.max(0, limit - count),
|
||||||
reset: Math.ceil(windowSeconds - (now % (windowSeconds * 1000)) / 1000),
|
reset: Math.ceil(windowSeconds - (now % windowMs) / 1000),
|
||||||
};
|
};
|
||||||
} catch {
|
|
||||||
// If Redis is unavailable, degrade gracefully instead of blocking user actions.
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
remaining: limit,
|
|
||||||
reset: windowSeconds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import Redis from "ioredis";
|
|
||||||
|
|
||||||
const globalForRedis = globalThis as unknown as {
|
|
||||||
redis?: Redis;
|
|
||||||
redisErrorBound?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function createRedis() {
|
|
||||||
const client = new Redis(process.env.REDIS_URL || "redis://localhost:6379", {
|
|
||||||
lazyConnect: true,
|
|
||||||
maxRetriesPerRequest: 1,
|
|
||||||
enableOfflineQueue: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!globalForRedis.redisErrorBound) {
|
|
||||||
client.on("error", (error) => {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
console.warn(`[redis] ${message}`);
|
|
||||||
});
|
|
||||||
globalForRedis.redisErrorBound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRedis() {
|
|
||||||
if (!globalForRedis.redis) {
|
|
||||||
globalForRedis.redis = createRedis();
|
|
||||||
}
|
|
||||||
|
|
||||||
return globalForRedis.redis;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
globalForRedis.redis = globalForRedis.redis ?? createRedis();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user