From 13c838bad7c43c903a234ae2cb5e4b8ac00ed71a Mon Sep 17 00:00:00 2001 From: leiyun Date: Tue, 4 Aug 2026 17:19:04 +0800 Subject: [PATCH] fix: align production API and in-page login --- .env.example | 5 ++++ README.md | 16 ++++++++++--- app/pages/draw/anime.vue | 15 ++++++++++-- app/pages/writing/naming.vue | 9 +++++++- ecosystem.config.cjs | 4 ++-- nuxt.config.js | 44 +++++++++++++++++++++++++++++++++++- restart.sh | 40 ++++++++++++++++++++++++++++++++ server/api/[...].js | 11 +++++++-- 8 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2b47be2 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Local development API. Copy this file to .env when a different address is needed. +NUXT_PUBLIC_API_BASE=http://192.168.31.168:16888 + +# Public site origin used for canonical URLs when configured. +NUXT_PUBLIC_SITE_URL=http://localhost:6888 diff --git a/README.md b/README.md index 2a2bd38..05bcccf 100644 --- a/README.md +++ b/README.md @@ -96,17 +96,27 @@ const res = await api.post('/user/login', { username, password }) | --- | --- | | `0` | 成功,数据在 `data` / `list` 字段 | | `1000` | 业务异常,错误信息在 `msg` | -| `1009` | 登录失效,客户端自动跳转 `/login` | +| `1009` | 登录失效,客户端清除登录态并打开当前页面的登录弹窗 | ### 配置后端地址 ```bash # 环境变量(优先级最高) -API_BASE=http://192.168.1.100:8360 pnpm run dev +NUXT_PUBLIC_API_BASE=http://192.168.1.100:8360 pnpm run dev -# 或直接修改 nuxt.config.js 中的 runtimeConfig.public.apiBase +# 未设置时,开发环境默认使用 http://192.168.31.168:16888 +# 也可以复制 .env.example 为 .env 后修改;不要直接修改 nuxt.config.js ``` +生产环境默认使用 `https://api.aionline.cc`。如需覆盖,必须使用公开的 HTTP(S) 地址: + +```bash +NUXT_PUBLIC_API_BASE=https://api.aionline.cc ./restart.sh +``` + +`restart.sh` 会在拉取和构建前校验该地址,拒绝 localhost、环回地址和局域网地址;同一变量会同时传入 Nuxt 构建、Nitro 运行时和 PM2,避免 SSR 与浏览器代理使用不同后端。 +发布脚本也会拒绝 `NODE_TLS_REJECT_UNAUTHORIZED=0`,生产机应保持 TLS 证书校验开启。 + ### 服务端代理 前端调 `/api/**` 会自动代理到 `ai_api`,路径示例: diff --git a/app/pages/draw/anime.vue b/app/pages/draw/anime.vue index b90c196..abfc4a2 100644 --- a/app/pages/draw/anime.vue +++ b/app/pages/draw/anime.vue @@ -407,7 +407,11 @@ const strengthCopy = computed(() => '推荐 0.6 - 0.8,保留人脸特征') // ==================== Upload ==================== function triggerUpload() { - if (!user.isLogin.value) { showToast('请先登录'); router.push('/login'); return } + if (!user.isLogin.value) { + showToast('请先登录') + user.showLogin.value = true + return + } fileInput.value?.click() } function onFileChange(e) { @@ -518,6 +522,9 @@ onMounted(() => { loadDemo() if (user.isLogin.value) loadBalance() }) +watch(() => user.isLogin.value, (loggedIn) => { + if (loggedIn) loadBalance() +}) onUnmounted(() => clearPoll()) // ==================== 历史记录 / 详情 ==================== @@ -661,7 +668,11 @@ async function startPoll() { } async function handleSubmit() { - if (!user.isLogin.value) { showToast('请先登录'); router.push('/login'); return } + if (!user.isLogin.value) { + showToast('请先登录') + user.showLogin.value = true + return + } if (uploading.value) { showToast('图片正在上传'); return } if (!form.imgs.length) { showToast('请上传参考图'); return } if (submitting.value) return diff --git a/app/pages/writing/naming.vue b/app/pages/writing/naming.vue index 1bb08a0..de4c0e5 100644 --- a/app/pages/writing/naming.vue +++ b/app/pages/writing/naming.vue @@ -855,6 +855,9 @@ async function loadBalance() { user.refreshBalance() } onMounted(() => { loadBilling(); if (user.isLogin.value) loadBalance() }) +watch(() => user.isLogin.value, (loggedIn) => { + if (loggedIn) loadBalance() +}) const submitting = ref(false) const questionTitle = ref('') @@ -880,7 +883,11 @@ const streamContent = ref('') const streamPreviewRef = ref(null) async function handleSubmit() { - if (!user.isLogin.value) { showToast('请先登录'); return router.push('/login') } + if (!user.isLogin.value) { + showToast('请先登录') + user.showLogin.value = true + return + } if (!form.surname) { showToast('请输入姓氏'); return } if (submitting.value) return syncBirthFields() diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index f222b8f..b08be09 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -1,14 +1,14 @@ const appName = process.env.APP_NAME || 'pc_nuxt' const host = process.env.HOST || '0.0.0.0' const port = process.env.PORT || '6888' -const apiBase = process.env.API_BASE || 'https://api.aionline.cc' +const apiBase = process.env.NUXT_PUBLIC_API_BASE || 'https://api.aionline.cc' const env = { NODE_ENV: 'production', HOST: host, PORT: port, NITRO_HOST: host, NITRO_PORT: port, - API_BASE: apiBase + NUXT_PUBLIC_API_BASE: apiBase } module.exports = { diff --git a/nuxt.config.js b/nuxt.config.js index 170a4b5..8c2eb20 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -1,5 +1,47 @@ import { fileURLToPath } from 'node:url' +const isProduction = process.env.NODE_ENV === 'production' +const defaultApiBase = isProduction + ? 'https://api.aionline.cc' + : 'http://192.168.31.168:16888' + +function resolveApiBase() { + const value = String(process.env.NUXT_PUBLIC_API_BASE || defaultApiBase).trim().replace(/\/+$/, '') + let url + try { + url = new URL(value) + } catch { + throw new Error(`[pc_nuxt] NUXT_PUBLIC_API_BASE 不是有效 URL:${value || '(empty)'}`) + } + + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error(`[pc_nuxt] NUXT_PUBLIC_API_BASE 仅支持 http/https:${value}`) + } + + const hostname = url.hostname.toLowerCase() + const isPrivateHost = hostname === 'localhost' + || hostname === '0.0.0.0' + || /^127\./.test(hostname) + || hostname === '[::1]' + || hostname.endsWith('.local') + || /^10\./.test(hostname) + || /^192\.168\./.test(hostname) + || /^169\.254\./.test(hostname) + || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) + + if (isProduction && isPrivateHost) { + throw new Error(`[pc_nuxt] 生产环境禁止使用本地/局域网 API:${value},请设置 NUXT_PUBLIC_API_BASE`) + } + + return value +} + +if (process.env.API_BASE && !process.env.NUXT_PUBLIC_API_BASE) { + console.warn('[pc_nuxt] API_BASE 已废弃,请改用 NUXT_PUBLIC_API_BASE') +} + +const apiBase = resolveApiBase() + // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: '2025-07-15', @@ -32,7 +74,7 @@ export default defineNuxtConfig({ runtimeConfig: { public: { // ThinkJS ai_api 地址,按环境覆盖 - apiBase: process.env.API_BASE || 'http://192.168.31.168:16888', + apiBase, // 邀请链接正式域名;未配置时自动使用当前访问域名。 siteUrl: process.env.NUXT_PUBLIC_SITE_URL || '', }, diff --git a/restart.sh b/restart.sh index 33fb921..4c70d7d 100644 --- a/restart.sh +++ b/restart.sh @@ -4,8 +4,11 @@ set -Eeuo pipefail APP_NAME="${APP_NAME:-pc_nuxt}" BRANCH="${BRANCH:-}" +NUXT_PUBLIC_API_BASE="${NUXT_PUBLIC_API_BASE:-https://api.aionline.cc}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export NUXT_PUBLIC_API_BASE + cd "$SCRIPT_DIR" log() { @@ -19,6 +22,40 @@ require_command() { fi } +validate_api_base() { + node - "$NUXT_PUBLIC_API_BASE" <<'NODE' +const value = process.argv[2] +let url +try { + url = new URL(value) +} catch { + console.error(`[pc_nuxt] Invalid NUXT_PUBLIC_API_BASE: ${value || '(empty)'}`) + process.exit(1) +} +const hostname = url.hostname.toLowerCase() +const isPrivate = hostname === 'localhost' + || hostname === '0.0.0.0' + || /^127\./.test(hostname) + || hostname === '[::1]' + || hostname.endsWith('.local') + || /^10\./.test(hostname) + || /^192\.168\./.test(hostname) + || /^169\.254\./.test(hostname) + || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) +if (!['http:', 'https:'].includes(url.protocol) || isPrivate) { + console.error(`[pc_nuxt] Production API must be a public http(s) URL: ${value}`) + process.exit(1) +} +NODE +} + +validate_tls_security() { + if [ "${NODE_TLS_REJECT_UNAUTHORIZED:-}" = "0" ]; then + log "Refusing deployment: NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification." + exit 1 + fi +} + ensure_clean_worktree() { if [ -n "$(git status --porcelain)" ]; then log "Working tree is not clean. Commit/stash/clean changes before deploy." @@ -71,12 +108,15 @@ main() { require_command node require_command pnpm require_command pm2 + validate_tls_security + validate_api_base resolve_branch log "Deploy start: $APP_NAME" log "Node: $(node -v 2>/dev/null || echo 'unknown')" log "pnpm: $(pnpm -v)" log "pm2: $(pm2 -v)" + log "API: $NUXT_PUBLIC_API_BASE" pull_latest_code install_dependencies diff --git a/server/api/[...].js b/server/api/[...].js index 8dac11a..c555f5b 100644 --- a/server/api/[...].js +++ b/server/api/[...].js @@ -8,12 +8,19 @@ * 1. 避免浏览器跨域 * 2. 服务端转发自动携带 cookie,保持登录态 * - * 环境变量 API_BASE 可覆盖目标地址(生产环境指向 https://api.aionline.cc) + * 环境变量 NUXT_PUBLIC_API_BASE 可覆盖目标地址(生产环境指向 https://api.aionline.cc) */ import { proxyRequest, sendWebResponse } from 'h3' export default defineEventHandler(async (event) => { - const apiBase = process.env.API_BASE || 'http://192.168.31.168:16888' + const config = useRuntimeConfig(event) + const apiBase = String(config.public.apiBase || '').replace(/\/+$/, '') + if (!apiBase) { + throw createError({ + statusCode: 500, + statusMessage: 'NUXT_PUBLIC_API_BASE is not configured', + }) + } // 去掉 /api 前缀,得到真实后端路径 let path = event.path.replace(/^\/api/, '') || '/'