From b9a75129f00c5413514c8a090018b02f5c75a84e Mon Sep 17 00:00:00 2001 From: whoa Date: Sun, 29 Mar 2026 17:36:14 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(webpanel):=E4=BF=AE=E5=A4=8D=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=A8=8B=E5=BA=8F=E6=97=A0=E6=B3=95=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E6=89=80=E6=9C=89=E5=AE=9E=E4=BE=8B=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E9=83=BD=E8=A2=AB=E9=80=80=E5=87=BA=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MSLX.WebPanel/src/components/UpdateModal.vue | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/MSLX.WebPanel/src/components/UpdateModal.vue b/MSLX.WebPanel/src/components/UpdateModal.vue index 982acf5..bf4fda5 100644 --- a/MSLX.WebPanel/src/components/UpdateModal.vue +++ b/MSLX.WebPanel/src/components/UpdateModal.vue @@ -1,5 +1,5 @@ + - + - +
diff --git a/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/auth.ts b/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/auth.ts new file mode 100644 index 0000000..42b6363 --- /dev/null +++ b/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/auth.ts @@ -0,0 +1,489 @@ +import { request } from '@/utils/request'; + +const API_BASE_URL = '/api/frp/chmlfrp'; +const ACCOUNT_OAUTH_ISSUER = 'https://account-api.qzhua.net'; +const ACCOUNT_OAUTH_CLIENT_ID = '019d4510d87276958d6248aed40407e3'; +const STORAGE_KEY = 'chmlfrp_user'; +const LEGACY_STORAGE_KEY = 'chmlfrp-user-token'; +const DEVICE_CODE_DEFAULT_SCOPE = 'profile email offline_access chmlfrp_api'; +const CHMLFRP_PROXY_AUTHORIZATION_HEADER = 'X-Chmlfrp-Authorization'; + +interface RawHttpResponse { + status: number; + body: string; +} + +export interface StoredChmlFrpUser { + username: string; + usergroup: string; + userimg?: string | null; + usertoken?: string; + accessToken?: string; + refreshToken?: string; + accessTokenExpiresAt?: number; + tokenType?: string; + tunnelCount?: number; + tunnel?: number; +} + +export interface ChmlFrpUserInfo { + id: number; + username: string; + password: string | null; + userimg: string; + qq: string; + email: string; + usertoken: string; + usergroup: string; + bandwidth: number; + tunnel: number; + realname: string; + integral: number; + term: string; + scgm: string; + regtime: string; + realname_count: number | null; + total_download: number | null; + total_upload: number | null; + tunnelCount: number; + totalCurConns: number; +} + +export interface ChmlFrpTunnel { + id: number; + name: string; + localip: string; + type: string; + nport: number; + dorp: string; + node: string; + ap: string; + uptime: string | null; + client_version: string | null; + today_traffic_in: number | null; + today_traffic_out: number | null; + cur_conns: number | null; + nodestate: string; + ip: string; + node_ip: string; + node_ipv6: string | null; + server_port: number; + node_token: string; + state?: string | boolean; +} + +export interface ChmlFrpNodeInfo { + id: number; + name: string; + area: string; + nodegroup: string; + notes: string; +} + +export interface CreateChmlFrpTunnelParams { + tunnelname: string; + node: string; + localip: string; + porttype: string; + localport: number; + encryption: boolean; + compression: boolean; + extraparams: string; + remoteport?: number; +} + +export interface DeviceAuthorizationResponse { + device_code: string; + user_code: string; + verification_uri: string; + verification_uri_complete?: string; + expires_in?: number; + interval?: number; +} + +export interface DeviceTokenResponse { + access_token?: string; + token_type?: string; + expires_in?: number; + refresh_token?: string; + scope?: string; + error?: string; + error_description?: string; + error_uri?: string; +} + +function normalizeStoredUser(user: StoredChmlFrpUser | null): StoredChmlFrpUser | null { + if (!user) { + return null; + } + + const normalized: StoredChmlFrpUser = { ...user }; + + if (normalized.accessTokenExpiresAt != null) { + const expiresAt = Number(normalized.accessTokenExpiresAt); + normalized.accessTokenExpiresAt = Number.isFinite(expiresAt) ? expiresAt : undefined; + } + + return normalized; +} + +function getOAuthUrl(path: string) { + return new URL(path, ACCOUNT_OAUTH_ISSUER).toString(); +} + +function getOAuthHeaders() { + return { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }; +} + +function getOAuthErrorMessage(response: DeviceTokenResponse | undefined, fallback: string) { + if (!response) { + return fallback; + } + + return response.error_description || response.error || fallback; +} + +function parseOAuthJson(response: RawHttpResponse, fallback: string): T { + try { + return JSON.parse(response.body) as T; + } catch { + const content = response.body.trim().toLowerCase(); + + if (content.startsWith(' { + const response = await fetch(getOAuthUrl(path), { + method: 'POST', + headers: getOAuthHeaders(), + body: body.toString(), + cache: 'no-store', + credentials: 'omit', + }); + + return { + status: response.status, + body: await response.text(), + }; +} + +function getLegacyApiToken(user: StoredChmlFrpUser | null) { + if (!user?.usertoken) { + return undefined; + } + + if (user.accessToken && user.usertoken === user.accessToken) { + return undefined; + } + + return user.usertoken; +} + +function getCurrentAccessToken(user: StoredChmlFrpUser | null) { + if (user?.accessToken?.trim()) { + return user.accessToken.trim(); + } + + return undefined; +} + +function isAccessTokenExpiring(user: StoredChmlFrpUser | null) { + const expiresAt = user?.accessTokenExpiresAt; + + if (!expiresAt) { + return false; + } + + return Date.now() >= expiresAt - 60_000; +} + +function toBearerHeader(token: string) { + return token.startsWith('Bearer ') ? token : `Bearer ${token}`; +} + +function normalizeApiData(response: any): T { + return response?.code === 200 ? response.data : response; +} + +export function getStoredChmlFrpUser(): StoredChmlFrpUser | null { + const saved = localStorage.getItem(STORAGE_KEY); + + if (saved) { + try { + return normalizeStoredUser(JSON.parse(saved) as StoredChmlFrpUser); + } catch { + localStorage.removeItem(STORAGE_KEY); + } + } + + const legacyToken = localStorage.getItem(LEGACY_STORAGE_KEY); + + if (!legacyToken) { + return null; + } + + const migratedUser: StoredChmlFrpUser = { + username: '', + usergroup: '', + usertoken: legacyToken, + }; + + saveStoredChmlFrpUser(migratedUser); + localStorage.removeItem(LEGACY_STORAGE_KEY); + + return migratedUser; +} + +export function saveStoredChmlFrpUser(user: StoredChmlFrpUser) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizeStoredUser(user))); + if (user.usertoken) { + localStorage.setItem(LEGACY_STORAGE_KEY, user.usertoken); + } else { + localStorage.removeItem(LEGACY_STORAGE_KEY); + } +} + +export function clearStoredChmlFrpUser() { + localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem(LEGACY_STORAGE_KEY); +} + +async function refreshAccessToken(refreshToken: string) { + const body = new URLSearchParams(); + body.set('grant_type', 'refresh_token'); + body.set('refresh_token', refreshToken); + body.set('client_id', ACCOUNT_OAUTH_CLIENT_ID); + + const response = await oauthRequest('/oauth2/token', body); + + return parseOAuthJson(response, '账户服务返回了无法解析的刷新响应'); +} + +async function ensureAuthenticatedUser() { + const storedUser = getStoredChmlFrpUser(); + + if (!storedUser) { + throw new Error('登录信息已过期,请重新授权'); + } + + const currentAccessToken = getCurrentAccessToken(storedUser); + + if (currentAccessToken) { + if (storedUser.refreshToken && isAccessTokenExpiring(storedUser)) { + const refreshed = await refreshAccessToken(storedUser.refreshToken); + + if (!refreshed.access_token) { + clearStoredChmlFrpUser(); + throw new Error(getOAuthErrorMessage(refreshed, '登录信息已过期,请重新授权')); + } + + const updatedUser: StoredChmlFrpUser = { + ...storedUser, + accessToken: refreshed.access_token, + refreshToken: refreshed.refresh_token || storedUser.refreshToken, + accessTokenExpiresAt: refreshed.expires_in + ? Date.now() + refreshed.expires_in * 1000 + : storedUser.accessTokenExpiresAt, + tokenType: refreshed.token_type || storedUser.tokenType || 'Bearer', + }; + + saveStoredChmlFrpUser(updatedUser); + + return { + storedUser: updatedUser, + accessToken: updatedUser.accessToken, + legacyToken: getLegacyApiToken(updatedUser), + }; + } + + return { + storedUser, + accessToken: currentAccessToken, + legacyToken: getLegacyApiToken(storedUser), + }; + } + + const legacyToken = getLegacyApiToken(storedUser); + + if (legacyToken) { + return { + storedUser, + legacyToken, + }; + } + + clearStoredChmlFrpUser(); + throw new Error('登录信息已过期,请重新授权'); +} + +export async function getChmlFrpAuthorizationHeader() { + const { accessToken, legacyToken } = await ensureAuthenticatedUser(); + return toBearerHeader(accessToken || legacyToken!); +} + +export async function createDeviceAuthorization(scope = DEVICE_CODE_DEFAULT_SCOPE) { + const body = new URLSearchParams(); + body.set('client_id', ACCOUNT_OAUTH_CLIENT_ID); + + const normalizedScope = scope + .split(/[,\s]+/) + .map((item) => item.trim()) + .filter(Boolean) + .join(' '); + + if (normalizedScope) { + body.set('scope', normalizedScope); + } + + const response = await oauthRequest('/oauth2/device_authorization', body); + const data = parseOAuthJson( + response, + '账户服务返回了无法解析的响应', + ); + + if (response.status >= 200 && response.status < 300 && data && 'device_code' in data) { + return data; + } + + throw new Error(getOAuthErrorMessage(data ?? undefined, '申请设备授权失败')); +} + +export async function exchangeDeviceCodeForToken(deviceCode: string) { + const body = new URLSearchParams(); + body.set('grant_type', 'urn:ietf:params:oauth:grant-type:device_code'); + body.set('device_code', deviceCode); + body.set('client_id', ACCOUNT_OAUTH_CLIENT_ID); + + const response = await oauthRequest('/oauth2/token', body); + + return parseOAuthJson(response, '账户服务返回了无法解析的令牌响应'); +} + +export async function fetchChmlFrpUserInfo(token?: string) { + const authorization = token ? toBearerHeader(token) : await getChmlFrpAuthorizationHeader(); + const response = await request.get( + { + url: `${API_BASE_URL}/userinfo`, + headers: { [CHMLFRP_PROXY_AUTHORIZATION_HEADER]: authorization }, + } + ); + + const data = normalizeApiData(response); + + if (data?.username) { + return data; + } + + throw new Error('未获取到有效的用户信息'); +} + +export async function loginWithAccessToken( + accessToken: string, + tokenResponse?: Pick, +) { + const userInfo = await fetchChmlFrpUserInfo(accessToken); + + return { + username: userInfo.username, + usergroup: userInfo.usergroup, + userimg: userInfo.userimg, + usertoken: userInfo.usertoken, + accessToken, + refreshToken: tokenResponse?.refresh_token, + accessTokenExpiresAt: tokenResponse?.expires_in + ? Date.now() + tokenResponse.expires_in * 1000 + : undefined, + tokenType: tokenResponse?.token_type || 'Bearer', + tunnelCount: userInfo.tunnelCount, + tunnel: userInfo.tunnel, + } satisfies StoredChmlFrpUser; +} + +export async function fetchChmlFrpTunnels() { + const authorization = await getChmlFrpAuthorizationHeader(); + const response = await request.get( + { + url: `${API_BASE_URL}/tunnel`, + headers: { [CHMLFRP_PROXY_AUTHORIZATION_HEADER]: authorization }, + } + ); + + const data = normalizeApiData(response); + + if (Array.isArray(data)) { + return data; + } + + throw new Error('获取隧道列表失败'); +} + +export async function fetchChmlFrpNodes() { + const authorization = await getChmlFrpAuthorizationHeader(); + const response = await request.get( + { + url: `${API_BASE_URL}/node`, + headers: { [CHMLFRP_PROXY_AUTHORIZATION_HEADER]: authorization }, + } + ); + + const data = normalizeApiData(response); + + if (Array.isArray(data)) { + return data; + } + + throw new Error('获取节点列表失败'); +} + +export async function createChmlFrpTunnel(params: CreateChmlFrpTunnelParams) { + const authorization = await getChmlFrpAuthorizationHeader(); + + return request.post( + { + url: `${API_BASE_URL}/create-tunnel`, + headers: { [CHMLFRP_PROXY_AUTHORIZATION_HEADER]: authorization }, + data: params, + } + ); +} + +export async function deleteChmlFrpTunnel(tunnelId: number) { + const authorization = await getChmlFrpAuthorizationHeader(); + + return request.get( + { + url: `${API_BASE_URL}/delete-tunnel?tunnelId=${tunnelId}`, + headers: { [CHMLFRP_PROXY_AUTHORIZATION_HEADER]: authorization }, + } + ); +} + +export async function fetchChmlFrpTunnelConfig(node: string, tunnelName: string) { + const authorization = await getChmlFrpAuthorizationHeader(); + const response = await request.get( + { + url: `${API_BASE_URL}/tunnel-config?node=${encodeURIComponent(node)}&tunnelName=${encodeURIComponent(tunnelName)}`, + headers: { [CHMLFRP_PROXY_AUTHORIZATION_HEADER]: authorization }, + } + ); + + const data = normalizeApiData(response); + + if (typeof data === 'string' && data) { + return data; + } + + throw new Error('获取配置失败:内容为空或格式异常'); +} diff --git a/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/components/CreateTunnelDialog.vue b/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/components/CreateTunnelDialog.vue index a267662..c8196c2 100644 --- a/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/components/CreateTunnelDialog.vue +++ b/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/components/CreateTunnelDialog.vue @@ -1,27 +1,18 @@ @@ -171,7 +143,12 @@ onMounted(() => { class="pt-2.5 overflow-x-hidden [&_.t-form__item]:!mb-[22px]" > - +
diff --git a/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/index.vue b/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/index.vue index 2100531..5b4846b 100644 --- a/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/index.vue +++ b/MSLX.WebPanel/src/pages/frp/createFrp/components/ChmlFrp/index.vue @@ -6,168 +6,230 @@ import { AddIcon, PlayCircleIcon, RefreshIcon, - KeyIcon, - LockOnIcon, } from 'tdesign-icons-vue-next'; import { changeUrl } from '@/router'; -import { onMounted, ref, computed } from 'vue'; -import { request } from '@/utils/request'; +import { onBeforeUnmount, onMounted, ref, computed } from 'vue'; import { MessagePlugin } from 'tdesign-vue-next'; import { convertIniToToml, createFrpTunnel } from '@/pages/frp/createFrp/utils/create'; import CreateTunnelDialog from './components/CreateTunnelDialog.vue'; +import { + clearStoredChmlFrpUser, + createDeviceAuthorization, + deleteChmlFrpTunnel, + exchangeDeviceCodeForToken, + fetchChmlFrpTunnelConfig, + fetchChmlFrpTunnels, + fetchChmlFrpUserInfo, + getStoredChmlFrpUser, + loginWithAccessToken, + saveStoredChmlFrpUser, + type ChmlFrpTunnel, + type ChmlFrpUserInfo, + type DeviceAuthorizationResponse, + type DeviceTokenResponse, + type StoredChmlFrpUser, +} from './auth'; const showCreateDialog = ref(false); -const chmlToken = ref(''); +const chmlUser = ref(null); -// 数据状态 const loading = ref(false); -const userInfo = ref(null); -const tunnels = ref([]); +const userInfo = ref(null); +const tunnels = ref([]); const selectedTunnelId = ref(null); -// 登录相关状态 -const loginType = ref('password'); -const loginForm = ref({ - username: '', - password: '', - token: '', -}); -const isLoggingIn = ref(false); +const authSession = ref(null); +const authMessage = ref('将在新标签页中打开授权页面'); +const authError = ref(''); +const isAuthorizing = ref(false); +const isPolling = ref(false); +let pollingTimer: number | null = null; const handleCreateSuccess = () => { - initDashboardData(); + void initDashboardData(); }; const currentTunnel = computed(() => { return tunnels.value.find((t) => t.id === selectedTunnelId.value) || null; }); +const hasChmlAuth = computed(() => { + return Boolean(chmlUser.value?.accessToken || chmlUser.value?.usertoken); +}); + onMounted(() => { - const token = localStorage.getItem('chmlfrp-user-token'); - if (token) { - chmlToken.value = token; - initDashboardData(); + const storedUser = getStoredChmlFrpUser(); + if (storedUser) { + chmlUser.value = storedUser; + void initDashboardData(false); } }); -// 账号密码登录 -async function handlePasswordLogin() { - if (!loginForm.value.username || !loginForm.value.password) { - MessagePlugin.warning('请填写完整的账号和密码'); - return; +onBeforeUnmount(() => { + stopPolling(); +}); + +function stopPolling() { + if (pollingTimer !== null) { + window.clearTimeout(pollingTimer); + pollingTimer = null; } - isLoggingIn.value = true; + isPolling.value = false; +} + +function resetAuthorizationState() { + stopPolling(); + authSession.value = null; + authMessage.value = '将在新标签页中打开授权页面'; + authError.value = ''; + isAuthorizing.value = false; +} + +function syncStoredUserInfo(nextUserInfo: ChmlFrpUserInfo) { + const currentUser = getStoredChmlFrpUser() || chmlUser.value; + const mergedUser: StoredChmlFrpUser = { + username: nextUserInfo.username, + usergroup: nextUserInfo.usergroup, + userimg: nextUserInfo.userimg, + usertoken: nextUserInfo.usertoken, + accessToken: currentUser?.accessToken, + refreshToken: currentUser?.refreshToken, + accessTokenExpiresAt: currentUser?.accessTokenExpiresAt, + tokenType: currentUser?.tokenType, + tunnelCount: nextUserInfo.tunnelCount, + tunnel: nextUserInfo.tunnel, + }; + + chmlUser.value = mergedUser; + saveStoredChmlFrpUser(mergedUser); +} + +async function finishLogin( + accessToken: string, + tokenResponse?: Pick, +) { + const authedUser = await loginWithAccessToken(accessToken, tokenResponse); + saveStoredChmlFrpUser(authedUser); + chmlUser.value = authedUser; + resetAuthorizationState(); + const loaded = await initDashboardData(); + if (loaded) { + MessagePlugin.success('ChmlFrp 授权成功'); + } +} + +function scheduleTokenPolling(deviceCode: string, intervalSeconds: number) { + stopPolling(); + pollingTimer = window.setTimeout(() => { + void pollToken(deviceCode, intervalSeconds); + }, intervalSeconds * 1000); +} + +async function pollToken(deviceCode: string, intervalSeconds: number) { + isPolling.value = true; try { - const res = await request.post( - { - url: '/login', - baseURL: 'https://cf-v2.uapis.cn', - data: { - username: loginForm.value.username, - password: loginForm.value.password, - }, - }, - { withToken: false }, - ); + const tokenResponse = await exchangeDeviceCodeForToken(deviceCode); + + if (tokenResponse.access_token) { + await finishLogin(tokenResponse.access_token, { + refresh_token: tokenResponse.refresh_token, + expires_in: tokenResponse.expires_in, + token_type: tokenResponse.token_type, + }); + return; + } - const resData = res?.code === 200 ? res.data : res; + if (tokenResponse.error === 'authorization_pending') { + authMessage.value = '请在浏览器中确认授权'; + scheduleTokenPolling(deviceCode, intervalSeconds); + return; + } - if (resData && resData.usertoken) { - MessagePlugin.success('登录成功'); - await handleTokenLogin(resData.usertoken); - } else { - MessagePlugin.error('登录失败:账号或密码错误'); + if (tokenResponse.error === 'slow_down') { + authMessage.value = '请求过于频繁,正在自动重试...'; + scheduleTokenPolling(deviceCode, intervalSeconds + 5); + return; + } + + if (tokenResponse.error === 'expired_token') { + stopPolling(); + authError.value = '这次设备授权已过期,请重新开始授权。'; + return; } + + if (tokenResponse.error === 'access_denied') { + stopPolling(); + authError.value = '你已取消本次授权,请重新开始。'; + return; + } + + throw new Error(tokenResponse.error_description || tokenResponse.error || '获取访问令牌失败'); } catch (e: any) { - const errorMsg = e.response?.data?.msg || e.msg || e.message || '未知错误'; - MessagePlugin.error('登录异常: ' + errorMsg); - } finally { - isLoggingIn.value = false; + stopPolling(); + authError.value = e?.message || '授权失败,请稍后重试'; } } -// Token (密钥) 登录验证 -async function handleTokenLogin(tokenToVerify?: string) { - const token = tokenToVerify || loginForm.value.token; - if (!token) { - MessagePlugin.warning('请输入密钥 Token'); +async function openAuthorizationPage(session = authSession.value) { + if (!session) { + authError.value = '请先开始授权流程'; return; } - isLoggingIn.value = true; - try { - const res = await request.get( - { - url: '/userinfo', - baseURL: 'https://cf-v2.uapis.cn', - headers: { Authorization: `Bearer ${token}` }, - }, - { withToken: false }, - ); - const resData = res?.code === 200 ? res.data : res; + const target = session.verification_uri_complete || session.verification_uri; - if (resData && resData.username) { - MessagePlugin.success('身份验证成功'); - chmlToken.value = token; - localStorage.setItem('chmlfrp-user-token', token); - userInfo.value = resData; - await initDashboardData(); - } else { - MessagePlugin.error('登录失败:未获取到有效的用户信息'); - } + if (!target) { + authError.value = '账户中心未返回可用的授权地址'; + return; + } + + changeUrl(target); + authMessage.value = '授权页已打开,完成授权后此页面会自动继续'; +} + +async function startDeviceAuthorization() { + resetAuthorizationState(); + isAuthorizing.value = true; + authMessage.value = '正在获取授权信息...'; + + try { + const session = await createDeviceAuthorization(); + authSession.value = session; + await openAuthorizationPage(session); + const intervalSeconds = Math.max(Number(session.interval || 5), 1); + void pollToken(session.device_code, intervalSeconds); } catch (e: any) { - const errorMsg = e.response?.data?.msg || e.msg || e.message || '未知错误'; - MessagePlugin.error('验证失败: ' + errorMsg); + stopPolling(); + authSession.value = null; + authError.value = e?.message || '启动授权失败'; } finally { - isLoggingIn.value = false; + isAuthorizing.value = false; } } -// 加载仪表盘数据 -async function initDashboardData() { +async function initDashboardData(showFailureMessage = true) { loading.value = true; try { - // 获取用户信息 - const userRes = await request.get( - { - url: '/userinfo', - baseURL: 'https://cf-v2.uapis.cn', - headers: { Authorization: `Bearer ${chmlToken.value}` }, - }, - { withToken: false }, - ); + const [nextUserInfo, nextTunnels] = await Promise.all([fetchChmlFrpUserInfo(), fetchChmlFrpTunnels()]); - const userData = userRes?.code === 200 ? userRes.data : userRes; + userInfo.value = nextUserInfo; + tunnels.value = nextTunnels || []; + syncStoredUserInfo(nextUserInfo); - if (userData && userData.username) { - userInfo.value = userData; - } else { - handleLogout(); - return; + if (tunnels.value.length === 0) { + selectedTunnelId.value = null; + } else if (!tunnels.value.some((item) => item.id === selectedTunnelId.value)) { + selectedTunnelId.value = tunnels.value[0].id; } - // 获取隧道列表 - const tunnelsRes = await request.get( - { - url: '/tunnel', - baseURL: 'https://cf-v2.uapis.cn', - headers: { Authorization: `Bearer ${chmlToken.value}` }, - }, - { withToken: false }, - ); - - const tunnelsData = tunnelsRes?.code === 200 ? tunnelsRes.data : tunnelsRes; - - if (Array.isArray(tunnelsData)) { - tunnels.value = tunnelsData || []; - if (tunnels.value.length > 0 && !selectedTunnelId.value) { - selectedTunnelId.value = tunnels.value[0].id; - } - } + return true; } catch (e: any) { - const errorMsg = e.response?.data?.msg || e.msg || e.message || 'Token失效或网络异常'; - MessagePlugin.error(`数据加载失败,已自动退出 ChmlFrp: ${errorMsg}`); - handleLogout(); + const errorMsg = e?.response?.data?.msg || e?.msg || e?.message || '授权已失效或网络异常'; + if (showFailureMessage) { + MessagePlugin.error(`ChmlFrp 数据加载失败:${errorMsg}`); + } + handleLogout(false); + return false; } finally { loading.value = false; } @@ -181,30 +243,16 @@ async function handleUseTunnel() { isAddingTunnel.value = true; try { - const res = await request.get( - { - url: `/tunnel_config?node=${encodeURIComponent(currentTunnel.value.node)}&tunnel_names=${encodeURIComponent(currentTunnel.value.name)}`, - baseURL: 'https://cf-v2.uapis.cn', - headers: { Authorization: `Bearer ${chmlToken.value}` }, - }, - { withToken: false }, + const configText = await fetchChmlFrpTunnelConfig(currentTunnel.value.node, currentTunnel.value.name); + await createFrpTunnel( + `${currentTunnel.value.name} | ${currentTunnel.value.node}`, + convertIniToToml(configText), + 'ChmlFrp', + 'toml', ); - - const configText = res?.code === 200 ? res.data : res; - - if (configText && typeof configText === 'string') { - await createFrpTunnel( - `${currentTunnel.value.name} | ${currentTunnel.value.node}`, - convertIniToToml(configText), - 'ChmlFrp', - 'toml', - ); - MessagePlugin.success('配置文件已成功加载'); - } else { - MessagePlugin.error('获取配置失败:内容为空或格式异常'); - } + MessagePlugin.success('配置文件已成功加载'); } catch (e: any) { - const errorMsg = e.response?.data?.msg || e.msg || e.message || '未知错误'; + const errorMsg = e?.response?.data?.msg || e?.msg || e?.message || '未知错误'; MessagePlugin.error(`获取配置异常: ${errorMsg}`); } finally { isAddingTunnel.value = false; @@ -215,18 +263,27 @@ const handleAddTunnel = () => { showCreateDialog.value = true; }; -function handleLogout() { - chmlToken.value = ''; +function handleLogout(showMessage = true) { + resetAuthorizationState(); + chmlUser.value = null; userInfo.value = null; tunnels.value = []; selectedTunnelId.value = null; - localStorage.removeItem('chmlfrp-user-token'); - MessagePlugin.success('已退出登录'); + clearStoredChmlFrpUser(); + if (showMessage) { + MessagePlugin.success('已断开 ChmlFrp 授权'); + } +} + +function handleLogoutConfirm() { + handleLogout(); } async function handleRefresh() { - await initDashboardData(); - MessagePlugin.success('数据已更新'); + const loaded = await initDashboardData(); + if (loaded) { + MessagePlugin.success('数据已更新'); + } } const isDeleting = ref(false); @@ -235,14 +292,7 @@ async function handleDeleteTunnel() { isDeleting.value = true; try { - const res: any = await request.get( - { - url: `/delete_tunnel?tunnelid=${currentTunnel.value.id}`, - baseURL: 'https://cf-v2.uapis.cn', - headers: { Authorization: `Bearer ${chmlToken.value}` }, - }, - { withToken: false }, - ); + const res: any = await deleteChmlFrpTunnel(currentTunnel.value.id); if (res && res.code && res.code !== 200) { throw new Error(res.msg || '删除失败'); @@ -262,7 +312,7 @@ async function handleDeleteTunnel() {