auth.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. /**
  2. * 更新发布工具 - 认证与服务器连接模块
  3. *
  4. * Copyright © 2025 imengyu.top imengyu-update-server
  5. */
  6. import { readFile, writeFile } from 'node:fs/promises';
  7. import path from 'node:path';
  8. import { dirname } from 'node:path';
  9. import { fileURLToPath } from 'node:url';
  10. import axios from 'axios';
  11. import md5 from 'md5';
  12. import { password, input } from '@inquirer/prompts';
  13. import { config } from './postConfig.mjs';
  14. import consola from 'consola';
  15. import { logError } from './utils.mjs';
  16. // 基础配置
  17. // ========================================
  18. const __filename = fileURLToPath(import.meta.url);
  19. const __dirname = dirname(__filename);
  20. export const constant = {
  21. ServerUrl: config.server,
  22. TokenSave: path.resolve(__dirname, './_token.json'),
  23. };
  24. let currentData = {
  25. token: '',
  26. refreshToken: '',
  27. identifier: '',
  28. };
  29. function initIdentifier() {
  30. if (!currentData.identifier)
  31. currentData.identifier = `commandClient${Math.floor(Math.random() * 1000)}`;
  32. }
  33. function getErrorMessage(e) {
  34. return e instanceof Error ? e.message : (typeof e === 'object' ? e : '' + e);
  35. }
  36. async function saveCurrentData() {
  37. await writeFile(constant.TokenSave, JSON.stringify(currentData));
  38. }
  39. // Axios 实例
  40. // ========================================
  41. export const axiosInstance = axios.create({
  42. baseURL: constant.ServerUrl,
  43. timeoutErrorMessage: '请求超时,请检查网络连接',
  44. responseType: 'json',
  45. withCredentials: false,
  46. validateStatus: () => true,
  47. });
  48. axiosInstance.interceptors.request.use((value) => {
  49. value.headers['authorization'] = JSON.stringify({
  50. auth: currentData?.token?.authName,
  51. validity: currentData?.token?.authKey,
  52. nonce: 'aaaaaaaaaa',
  53. identifier: currentData.identifier,
  54. key: 'abc123',
  55. });
  56. value.url =
  57. value.url +
  58. (value.url.includes('?') ? '&' : '?') +
  59. `identifier=${currentData.identifier}`;
  60. return value;
  61. });
  62. let refreshPromise = null;
  63. async function tryRefreshToken() {
  64. if (!currentData.refreshToken) return false;
  65. if (refreshPromise) return refreshPromise;
  66. refreshPromise = (async () => {
  67. try {
  68. const res = await axios.post(
  69. `${constant.ServerUrl}auth/refresh?identifier=${currentData.identifier}`,
  70. { refreshToken: currentData.refreshToken },
  71. { responseType: 'json', validateStatus: () => true }
  72. );
  73. if (res.data && res.data.success) {
  74. currentData.token = {
  75. authName: res.data.data.authName,
  76. authKey: res.data.data.authKey,
  77. };
  78. if (res.data.data.refreshToken)
  79. currentData.refreshToken = res.data.data.refreshToken;
  80. await saveCurrentData();
  81. return true;
  82. } else
  83. return false;
  84. } catch {
  85. // 刷新失败
  86. }
  87. return false;
  88. })();
  89. try {
  90. return await refreshPromise;
  91. } finally {
  92. refreshPromise = null;
  93. }
  94. }
  95. axiosInstance.interceptors.response.use((value) => {
  96. if (value.data.success)
  97. return value.data;
  98. const err = value.data;
  99. err.__config = value.config;
  100. return Promise.reject(err);
  101. }, (error) => {
  102. return Promise.reject(error);
  103. });
  104. axiosInstance.interceptors.response.use(null, async (error) => {
  105. const code = error?.code;
  106. if (
  107. (code === 40102 || code === 40103) &&
  108. currentData.refreshToken &&
  109. !error.__retried
  110. ) {
  111. const refreshed = await tryRefreshToken();
  112. if (refreshed) {
  113. const cfg = error.__config || error.config;
  114. if (cfg) {
  115. error.__retried = true;
  116. return axiosInstance.request(cfg);
  117. }
  118. }
  119. }
  120. return Promise.reject(error);
  121. });
  122. // 认证初始化
  123. // ========================================
  124. /**
  125. * 初始化认证模块,从本地加载 token,并检查登录状态
  126. * @returns {Promise<void>}
  127. */
  128. export async function initAuth() {
  129. try {
  130. const res = await readFile(constant.TokenSave);
  131. const parsed = JSON.parse(res);
  132. if (parsed && typeof parsed === 'object') {
  133. currentData = { ...currentData, ...parsed };
  134. }
  135. } catch {
  136. // 无 token 文件时使用默认值
  137. }
  138. initIdentifier();
  139. if (currentData.token) {
  140. try {
  141. const res = await axiosInstance.get('/user/user-info');
  142. if (res?.data) {
  143. consola.info(`当前登录用户: ${res.data.name || res.data.user} (ID: ${res.data.id})`);
  144. return;
  145. }
  146. } catch {
  147. // 登录已过期,尝试刷新 token
  148. }
  149. if (currentData.refreshToken) {
  150. consola.info('尝试刷新 token');
  151. try {
  152. const res = await tryRefreshToken();
  153. if (res) {
  154. consola.success('刷新登录成功');
  155. return;
  156. }
  157. } catch (e) {
  158. logError('刷新 token 失败', e);
  159. }
  160. }
  161. }
  162. consola.warn('未登录,进入登录界面');
  163. await interactiveLogin();
  164. }
  165. /**
  166. * 获取当前认证数据(只读)
  167. */
  168. export function getCurrentData() {
  169. return { ...currentData };
  170. }
  171. // 登录相关
  172. // ========================================
  173. /**
  174. * 执行登录请求并保存 token
  175. * @param {string} user 用户名
  176. * @param {string} pass 密码
  177. */
  178. async function doLogin(user, pass) {
  179. const res = await axiosInstance.post('/auth?rember=true', {
  180. method: 'key',
  181. key: `${user}@${md5(pass)}`,
  182. });
  183. currentData.token = {
  184. authName: res.data.authName,
  185. authKey: res.data.authKey,
  186. };
  187. if (res.data.refreshToken)
  188. currentData.refreshToken = res.data.refreshToken;
  189. await saveCurrentData();
  190. }
  191. /**
  192. * 交互式登录,循环直至登录成功或用户取消
  193. */
  194. async function interactiveLogin() {
  195. while (true) {
  196. try {
  197. const user = await input({ message: '输入用户名' });
  198. const pass = await password({ message: '输入密码' });
  199. await doLogin(user, pass);
  200. consola.success('登录成功');
  201. return;
  202. } catch (e) {
  203. if (e?.name === 'ExitPromptError' || e?.name === 'CancelPromptError')
  204. return;
  205. logError('登录失败', getErrorMessage(e));
  206. }
  207. }
  208. }
  209. /**
  210. * 检查登录状态
  211. */
  212. export async function checkLogged() {
  213. try {
  214. await axiosInstance.get('/auth');
  215. consola.success('已登录');
  216. } catch (e) {
  217. logError('获取状态失败:', getErrorMessage(e));
  218. }
  219. }
  220. /**
  221. * 登录
  222. * @param {string} user 用户名
  223. */
  224. export async function login(user) {
  225. try {
  226. const username = user || await input({ message: '输入用户名' });
  227. const pass = await password({ message: '输入密码' });
  228. await doLogin(username, pass);
  229. consola.success('登录成功');
  230. } catch (e) {
  231. logError('登录失败', getErrorMessage(e));
  232. }
  233. }
  234. /**
  235. * 退出登录
  236. */
  237. export async function logout() {
  238. currentData.token = '';
  239. currentData.refreshToken = '';
  240. await saveCurrentData();
  241. try {
  242. await axiosInstance.delete('/auth');
  243. consola.success('退出登录成功');
  244. } catch (e) {
  245. logError('退出登录失败', getErrorMessage(e));
  246. }
  247. }