mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3437 from imfangwenjie/fix/api-base-fetch
fix(frontend): use configured API base for direct requests
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* - Dashboard overview (raw path)
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client'
|
||||
import { apiClient, buildGatewayUrl } from '../client'
|
||||
import type { PaginatedResponse } from '@/types'
|
||||
|
||||
export type OpsQueryMode = 'auto' | 'raw' | 'preagg'
|
||||
@@ -593,9 +593,10 @@ export function subscribeQPS(onMessage: (data: any) => void, options: SubscribeQ
|
||||
|
||||
isConnecting = true
|
||||
setStatus(hasConnectedOnce ? 'reconnecting' : 'connecting')
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsBaseUrl = options.wsBaseUrl || import.meta.env.VITE_WS_BASE_URL || window.location.host
|
||||
const wsURL = new URL(`${protocol}//${wsBaseUrl}/api/v1/admin/ops/ws/qps`)
|
||||
const wsBaseUrl = options.wsBaseUrl || import.meta.env.VITE_WS_BASE_URL
|
||||
const wsURL = wsBaseUrl
|
||||
? new URL(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${wsBaseUrl}/api/v1/admin/ops/ws/qps`)
|
||||
: new URL(buildGatewayUrl('/api/v1/admin/ops/ws/qps').replace(/^http/, 'ws'))
|
||||
|
||||
// Do NOT put admin JWT in the URL query string (it can leak via access logs, proxies, etc).
|
||||
// Browsers cannot set Authorization headers for WebSockets, so we pass the token via
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import type { ApiResponse } from '@/types'
|
||||
import { getLocale } from '@/i18n'
|
||||
import { getAPIBaseURL } from './url'
|
||||
export { buildApiUrl, buildGatewayUrl } from './url'
|
||||
|
||||
// ==================== Axios Instance Configuration ====================
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'
|
||||
|
||||
export const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL: getAPIBaseURL(),
|
||||
withCredentials: true,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
@@ -203,7 +203,7 @@ apiClient.interceptors.response.use(
|
||||
try {
|
||||
// Call refresh endpoint directly to avoid circular dependency
|
||||
const refreshResponse = await axios.post(
|
||||
`${API_BASE_URL}/auth/refresh`,
|
||||
`${getAPIBaseURL()}/auth/refresh`,
|
||||
{ refresh_token: refreshToken },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
* Setup API endpoints
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import { buildGatewayUrl } from './url'
|
||||
|
||||
// Create a separate client for setup endpoints (not under /api/v1)
|
||||
const setupClient = axios.create({
|
||||
baseURL: '',
|
||||
baseURL: buildGatewayUrl('/').replace(/\/+$/, ''),
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
export function getAPIBaseURL(): string {
|
||||
return String(API_BASE_URL || '/api/v1')
|
||||
}
|
||||
|
||||
export function buildApiUrl(path: string): string {
|
||||
const base = getAPIBaseURL().replace(/\/+$/, '')
|
||||
let suffix = normalizePath(path)
|
||||
if (suffix === '/api/v1') {
|
||||
suffix = ''
|
||||
} else if (suffix.startsWith('/api/v1/')) {
|
||||
suffix = suffix.slice('/api/v1'.length)
|
||||
}
|
||||
return `${base}${suffix}`
|
||||
}
|
||||
|
||||
export function buildGatewayUrl(path: string): string {
|
||||
const suffix = normalizePath(path)
|
||||
try {
|
||||
const origin =
|
||||
typeof window === 'undefined'
|
||||
? new URL(getAPIBaseURL()).origin
|
||||
: new URL(getAPIBaseURL(), window.location.origin).origin
|
||||
return `${origin}${suffix}`
|
||||
} catch {
|
||||
return suffix
|
||||
}
|
||||
}
|
||||
@@ -249,6 +249,7 @@ import Select from '@/components/common/Select.vue'
|
||||
import TextArea from '@/components/common/TextArea.vue'
|
||||
import { Icon } from '@/components/icons'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { buildApiUrl } from '@/api/client'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, ClaudeModel } from '@/types'
|
||||
|
||||
@@ -417,8 +418,8 @@ const startTest = async () => {
|
||||
abortController = new AbortController()
|
||||
|
||||
try {
|
||||
// Create EventSource for SSE
|
||||
const url = `/api/v1/admin/accounts/${props.account.id}/test`
|
||||
// Use the configured API base; EventSource does not support POST.
|
||||
const url = buildApiUrl(`/admin/accounts/${props.account.id}/test`)
|
||||
|
||||
// Use fetch with streaming for SSE since EventSource doesn't support POST
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -238,6 +238,7 @@ import Select from '@/components/common/Select.vue'
|
||||
import TextArea from '@/components/common/TextArea.vue'
|
||||
import { Icon } from '@/components/icons'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { buildApiUrl } from '@/api/client'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, ClaudeModel } from '@/types'
|
||||
|
||||
@@ -399,8 +400,8 @@ const startTest = async () => {
|
||||
abortController = new AbortController()
|
||||
|
||||
try {
|
||||
// Create EventSource for SSE
|
||||
const url = `/api/v1/admin/accounts/${props.account.id}/test`
|
||||
// Use the configured API base; EventSource does not support POST.
|
||||
const url = buildApiUrl(`/admin/accounts/${props.account.id}/test`)
|
||||
|
||||
// Use fetch with streaming for SSE since EventSource doesn't support POST
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -5915,7 +5915,7 @@ export default {
|
||||
apiBaseUrl: 'API Base URL',
|
||||
apiBaseUrlPlaceholder: 'https://api.example.com',
|
||||
apiBaseUrlHint:
|
||||
'Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.',
|
||||
'Used for "Use Key", "Import to CC Switch", and callback URL suggestions. Leave empty to use current site URL.',
|
||||
tablePreferencesTitle: 'Global Table Preferences',
|
||||
tablePreferencesDescription: 'Configure default pagination behavior for shared table components',
|
||||
tableDefaultPageSize: 'Default Rows Per Page',
|
||||
|
||||
@@ -6067,7 +6067,7 @@ export default {
|
||||
siteSubtitleHint: '显示在登录和注册页面',
|
||||
siteSubtitlePlaceholder: '订阅转 API 转换平台',
|
||||
apiBaseUrl: 'API 端点地址',
|
||||
apiBaseUrlHint: '用于"使用密钥"和"导入到 CC Switch"功能,留空则使用当前站点地址',
|
||||
apiBaseUrlHint: '用于"使用密钥"、"导入到 CC Switch"和回调地址建议,留空则使用当前站点地址',
|
||||
apiBaseUrlPlaceholder: 'https://api.example.com',
|
||||
tablePreferencesTitle: '通用表格设置',
|
||||
tablePreferencesDescription: '设置后台与用户侧表格组件的默认分页行为',
|
||||
|
||||
@@ -422,6 +422,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores'
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { buildGatewayUrl } from '@/api/client'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
@@ -856,7 +857,7 @@ function getBrowserTimezone(): string {
|
||||
|
||||
async function fetchUsage(key: string) {
|
||||
const dateParams = getDateParams()
|
||||
const url = '/v1/usage' + (dateParams ? '?' + dateParams : '')
|
||||
const url = buildGatewayUrl('/v1/usage') + (dateParams ? '?' + dateParams : '')
|
||||
const res = await fetch(url, {
|
||||
headers: { 'Authorization': 'Bearer ' + key },
|
||||
})
|
||||
|
||||
@@ -8441,13 +8441,15 @@ const addQuotaNotifyEmail = () => {
|
||||
const currentOrigin =
|
||||
typeof window !== "undefined" ? window.location.origin : "";
|
||||
|
||||
function buildApiCallbackUrl(path: string): string {
|
||||
const base = (form.api_base_url || currentOrigin).replace(/\/+$/, "");
|
||||
const apiRoot = base.endsWith("/api/v1") ? base : `${base}/api/v1`;
|
||||
return `${apiRoot}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
// LinuxDo OAuth redirect URL suggestion
|
||||
const linuxdoRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const origin =
|
||||
window.location.origin ||
|
||||
`${window.location.protocol}//${window.location.host}`;
|
||||
return `${origin}/api/v1/auth/oauth/linuxdo/callback`;
|
||||
return buildApiCallbackUrl("/auth/oauth/linuxdo/callback");
|
||||
});
|
||||
|
||||
async function setAndCopyLinuxdoRedirectUrl() {
|
||||
@@ -8464,19 +8466,11 @@ async function setAndCopyLinuxdoRedirectUrl() {
|
||||
type EmailOAuthProvider = "github" | "google";
|
||||
|
||||
const githubOAuthRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const origin =
|
||||
window.location.origin ||
|
||||
`${window.location.protocol}//${window.location.host}`;
|
||||
return `${origin}/api/v1/auth/oauth/github/callback`;
|
||||
return buildApiCallbackUrl("/auth/oauth/github/callback");
|
||||
});
|
||||
|
||||
const googleOAuthRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const origin =
|
||||
window.location.origin ||
|
||||
`${window.location.protocol}//${window.location.host}`;
|
||||
return `${origin}/api/v1/auth/oauth/google/callback`;
|
||||
return buildApiCallbackUrl("/auth/oauth/google/callback");
|
||||
});
|
||||
|
||||
async function setAndCopyEmailOAuthRedirectUrl(provider: EmailOAuthProvider) {
|
||||
@@ -8498,11 +8492,7 @@ async function setAndCopyEmailOAuthRedirectUrl(provider: EmailOAuthProvider) {
|
||||
}
|
||||
|
||||
const wechatRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const origin =
|
||||
window.location.origin ||
|
||||
`${window.location.protocol}//${window.location.host}`;
|
||||
return `${origin}/api/v1/auth/oauth/wechat/callback`;
|
||||
return buildApiCallbackUrl("/auth/oauth/wechat/callback");
|
||||
});
|
||||
|
||||
function syncWeChatConnectMode(preferredMode?: WeChatConnectMode) {
|
||||
@@ -8567,11 +8557,7 @@ async function setAndCopyWeChatRedirectUrl() {
|
||||
}
|
||||
|
||||
const oidcRedirectUrlSuggestion = computed(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const origin =
|
||||
window.location.origin ||
|
||||
`${window.location.protocol}//${window.location.host}`;
|
||||
return `${origin}/api/v1/auth/oauth/oidc/callback`;
|
||||
return buildApiCallbackUrl("/auth/oauth/oidc/callback");
|
||||
});
|
||||
|
||||
async function setAndCopyOIDCRedirectUrl() {
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { testDatabase, testRedis, install, type InstallRequest } from '@/api/setup'
|
||||
import { buildGatewayUrl } from '@/api/client'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import Toggle from '@/components/common/Toggle.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -644,7 +645,7 @@ async function waitForServiceRestart() {
|
||||
try {
|
||||
// Use setup status endpoint as it tells us the real mode
|
||||
// Service might return 404 or connection refused while restarting
|
||||
const response = await fetch('/setup/status', {
|
||||
const response = await fetch(buildGatewayUrl('/setup/status'), {
|
||||
method: 'GET',
|
||||
cache: 'no-store'
|
||||
})
|
||||
|
||||
@@ -124,6 +124,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useAdminSettingsStore } from '@/stores/adminSettings'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { buildApiUrl } from '@/api/client'
|
||||
import { buildEmbeddedUrl, detectTheme } from '@/utils/embedded-url'
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
@@ -217,7 +218,7 @@ function buildPageImageUrl(slug: string, src: string): string {
|
||||
.filter((part) => part && part !== '.')
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join('/')
|
||||
return `/api/v1/pages/${encodeURIComponent(slug)}/images/${encodedPath}${suffix}`
|
||||
return buildApiUrl(`/pages/${encodeURIComponent(slug)}/images/${encodedPath}${suffix}`)
|
||||
}
|
||||
|
||||
async function fetchAndRenderMarkdown(slug: string) {
|
||||
@@ -225,7 +226,7 @@ async function fetchAndRenderMarkdown(slug: string) {
|
||||
tocItems.value = []
|
||||
activeHeadingId.value = ''
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/pages/${encodeURIComponent(slug)}`, {
|
||||
const resp = await fetch(buildApiUrl(`/pages/${encodeURIComponent(slug)}`), {
|
||||
headers: authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {},
|
||||
})
|
||||
if (!resp.ok) {
|
||||
|
||||
@@ -58,6 +58,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { isMobileDevice } from '@/utils/device'
|
||||
import { buildApiUrl } from '@/api/client'
|
||||
|
||||
interface StripeWithWechatPay {
|
||||
confirmWechatPayPayment(clientSecret: string, options: Record<string, unknown>): Promise<{ error?: { message?: string }; paymentIntent?: { status: string } }>
|
||||
@@ -152,7 +153,7 @@ function startPolling() {
|
||||
try {
|
||||
const token = document.cookie.split('; ').find(c => c.startsWith('token='))?.split('=')[1]
|
||||
|| localStorage.getItem('token') || ''
|
||||
const res = await fetch('/api/v1/payment/orders/' + orderId, {
|
||||
const res = await fetch(buildApiUrl(`/payment/orders/${orderId}`), {
|
||||
headers: token ? { Authorization: 'Bearer ' + token } : {},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user