/** * 更新发布工具 - 认证与服务器连接模块 * * Copyright © 2025 imengyu.top imengyu-update-server */ import { readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import axios from 'axios'; import md5 from 'md5'; import { password, input } from '@inquirer/prompts'; import { config } from './postConfig.mjs'; import consola from 'consola'; import { logError } from './utils.mjs'; // 基础配置 // ======================================== const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export const constant = { ServerUrl: config.server, TokenSave: path.resolve(__dirname, './_token.json'), }; let currentData = { token: '', refreshToken: '', identifier: '', }; function initIdentifier() { if (!currentData.identifier) currentData.identifier = `commandClient${Math.floor(Math.random() * 1000)}`; } function getErrorMessage(e) { return e instanceof Error ? e.message : (typeof e === 'object' ? e : '' + e); } async function saveCurrentData() { await writeFile(constant.TokenSave, JSON.stringify(currentData)); } // Axios 实例 // ======================================== export const axiosInstance = axios.create({ baseURL: constant.ServerUrl, timeoutErrorMessage: '请求超时,请检查网络连接', responseType: 'json', withCredentials: false, validateStatus: () => true, }); axiosInstance.interceptors.request.use((value) => { value.headers['authorization'] = JSON.stringify({ auth: currentData?.token?.authName, validity: currentData?.token?.authKey, nonce: 'aaaaaaaaaa', identifier: currentData.identifier, key: 'abc123', }); value.url = value.url + (value.url.includes('?') ? '&' : '?') + `identifier=${currentData.identifier}`; return value; }); let refreshPromise = null; async function tryRefreshToken() { if (!currentData.refreshToken) return false; if (refreshPromise) return refreshPromise; refreshPromise = (async () => { try { const res = await axios.post( `${constant.ServerUrl}auth/refresh?identifier=${currentData.identifier}`, { refreshToken: currentData.refreshToken }, { responseType: 'json', validateStatus: () => true } ); if (res.data && res.data.success) { currentData.token = { authName: res.data.data.authName, authKey: res.data.data.authKey, }; if (res.data.data.refreshToken) currentData.refreshToken = res.data.data.refreshToken; await saveCurrentData(); return true; } else return false; } catch { // 刷新失败 } return false; })(); try { return await refreshPromise; } finally { refreshPromise = null; } } axiosInstance.interceptors.response.use((value) => { if (value.data.success) return value.data; const err = value.data; err.__config = value.config; return Promise.reject(err); }, (error) => { return Promise.reject(error); }); axiosInstance.interceptors.response.use(null, async (error) => { const code = error?.code; if ( (code === 40102 || code === 40103) && currentData.refreshToken && !error.__retried ) { const refreshed = await tryRefreshToken(); if (refreshed) { const cfg = error.__config || error.config; if (cfg) { error.__retried = true; return axiosInstance.request(cfg); } } } return Promise.reject(error); }); // 认证初始化 // ======================================== /** * 初始化认证模块,从本地加载 token,并检查登录状态 * @returns {Promise} */ export async function initAuth() { try { const res = await readFile(constant.TokenSave); const parsed = JSON.parse(res); if (parsed && typeof parsed === 'object') { currentData = { ...currentData, ...parsed }; } } catch { // 无 token 文件时使用默认值 } initIdentifier(); if (currentData.token) { try { const res = await axiosInstance.get('/user/user-info'); if (res?.data) { consola.info(`当前登录用户: ${res.data.name || res.data.user} (ID: ${res.data.id})`); return; } } catch { // 登录已过期,尝试刷新 token } if (currentData.refreshToken) { consola.info('尝试刷新 token'); try { const res = await tryRefreshToken(); if (res) { consola.success('刷新登录成功'); return; } } catch (e) { logError('刷新 token 失败', e); } } } consola.warn('未登录,进入登录界面'); await interactiveLogin(); } /** * 获取当前认证数据(只读) */ export function getCurrentData() { return { ...currentData }; } // 登录相关 // ======================================== /** * 执行登录请求并保存 token * @param {string} user 用户名 * @param {string} pass 密码 */ async function doLogin(user, pass) { const res = await axiosInstance.post('/auth?rember=true', { method: 'key', key: `${user}@${md5(pass)}`, }); currentData.token = { authName: res.data.authName, authKey: res.data.authKey, }; if (res.data.refreshToken) currentData.refreshToken = res.data.refreshToken; await saveCurrentData(); } /** * 交互式登录,循环直至登录成功或用户取消 */ async function interactiveLogin() { while (true) { try { const user = await input({ message: '输入用户名' }); const pass = await password({ message: '输入密码' }); await doLogin(user, pass); consola.success('登录成功'); return; } catch (e) { if (e?.name === 'ExitPromptError' || e?.name === 'CancelPromptError') return; logError('登录失败', getErrorMessage(e)); } } } /** * 检查登录状态 */ export async function checkLogged() { try { await axiosInstance.get('/auth'); consola.success('已登录'); } catch (e) { logError('获取状态失败:', getErrorMessage(e)); } } /** * 登录 * @param {string} user 用户名 */ export async function login(user) { try { const username = user || await input({ message: '输入用户名' }); const pass = await password({ message: '输入密码' }); await doLogin(username, pass); consola.success('登录成功'); } catch (e) { logError('登录失败', getErrorMessage(e)); } } /** * 退出登录 */ export async function logout() { currentData.token = ''; currentData.refreshToken = ''; await saveCurrentData(); try { await axiosInstance.delete('/auth'); consola.success('退出登录成功'); } catch (e) { logError('退出登录失败', getErrorMessage(e)); } }