auth.mjs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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. <<<<<<< HEAD
  27. refreshToken: '',
  28. =======
  29. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  30. identifier: '',
  31. };
  32. function initIdentifier() {
  33. if (!currentData.identifier)
  34. currentData.identifier = `commandClient${Math.floor(Math.random() * 1000)}`;
  35. }
  36. function getErrorMessage(e) {
  37. return e instanceof Error ? e.message : (typeof e === 'object' ? e : '' + e);
  38. }
  39. <<<<<<< HEAD
  40. async function saveCurrentData() {
  41. await writeFile(constant.TokenSave, JSON.stringify(currentData));
  42. }
  43. =======
  44. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  45. // Axios 实例
  46. // ========================================
  47. export const axiosInstance = axios.create({
  48. baseURL: constant.ServerUrl,
  49. timeoutErrorMessage: '请求超时,请检查网络连接',
  50. responseType: 'json',
  51. withCredentials: false,
  52. validateStatus: () => true,
  53. });
  54. axiosInstance.interceptors.request.use((value) => {
  55. value.headers['authorization'] = JSON.stringify({
  56. auth: currentData?.token?.authName,
  57. validity: currentData?.token?.authKey,
  58. nonce: 'aaaaaaaaaa',
  59. identifier: currentData.identifier,
  60. key: 'abc123',
  61. });
  62. value.url =
  63. value.url +
  64. (value.url.includes('?') ? '&' : '?') +
  65. `identifier=${currentData.identifier}`;
  66. return value;
  67. });
  68. <<<<<<< HEAD
  69. let refreshPromise = null;
  70. async function tryRefreshToken() {
  71. if (!currentData.refreshToken) return false;
  72. if (refreshPromise) return refreshPromise;
  73. refreshPromise = (async () => {
  74. try {
  75. const res = await axios.post(
  76. `${constant.ServerUrl}auth/refresh?identifier=${currentData.identifier}`,
  77. { refreshToken: currentData.refreshToken },
  78. { responseType: 'json', validateStatus: () => true }
  79. );
  80. if (res.data && res.data.success) {
  81. currentData.token = {
  82. authName: res.data.data.authName,
  83. authKey: res.data.data.authKey,
  84. };
  85. if (res.data.data.refreshToken)
  86. currentData.refreshToken = res.data.data.refreshToken;
  87. await saveCurrentData();
  88. return true;
  89. } else
  90. return false;
  91. } catch {
  92. // 刷新失败
  93. }
  94. return false;
  95. })();
  96. try {
  97. return await refreshPromise;
  98. } finally {
  99. refreshPromise = null;
  100. }
  101. }
  102. axiosInstance.interceptors.response.use((value) => {
  103. if (value.data.success)
  104. return value.data;
  105. const err = value.data;
  106. err.__config = value.config;
  107. return Promise.reject(err);
  108. }, (error) => {
  109. return Promise.reject(error);
  110. });
  111. axiosInstance.interceptors.response.use(null, async (error) => {
  112. const code = error?.code;
  113. if (
  114. (code === 40102 || code === 40103) &&
  115. currentData.refreshToken &&
  116. !error.__retried
  117. ) {
  118. const refreshed = await tryRefreshToken();
  119. if (refreshed) {
  120. const cfg = error.__config || error.config;
  121. if (cfg) {
  122. error.__retried = true;
  123. return axiosInstance.request(cfg);
  124. }
  125. }
  126. }
  127. return Promise.reject(error);
  128. =======
  129. axiosInstance.interceptors.response.use((value) => {
  130. if (value.data.success) return value.data;
  131. else return Promise.reject(value.data);
  132. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  133. });
  134. // 认证初始化
  135. // ========================================
  136. /**
  137. <<<<<<< HEAD
  138. * 初始化认证模块,从本地加载 token,并检查登录状态
  139. =======
  140. * 初始化认证模块,从本地加载 token
  141. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  142. * @returns {Promise<void>}
  143. */
  144. export async function initAuth() {
  145. try {
  146. const res = await readFile(constant.TokenSave);
  147. const parsed = JSON.parse(res);
  148. if (parsed && typeof parsed === 'object') {
  149. currentData = { ...currentData, ...parsed };
  150. }
  151. } catch {
  152. // 无 token 文件时使用默认值
  153. }
  154. initIdentifier();
  155. <<<<<<< HEAD
  156. if (currentData.token) {
  157. try {
  158. const res = await axiosInstance.get('/user/user-info');
  159. if (res?.data) {
  160. consola.info(`当前登录用户: ${res.data.name || res.data.user} (ID: ${res.data.id})`);
  161. return;
  162. }
  163. } catch {
  164. // 登录已过期,尝试刷新 token
  165. }
  166. if (currentData.refreshToken) {
  167. consola.info('尝试刷新 token');
  168. try {
  169. const res = await tryRefreshToken();
  170. if (res) {
  171. consola.success('刷新登录成功');
  172. return;
  173. }
  174. } catch (e) {
  175. logError('刷新 token 失败', e);
  176. }
  177. }
  178. }
  179. consola.warn('未登录,进入登录界面');
  180. await interactiveLogin();
  181. =======
  182. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  183. }
  184. /**
  185. * 获取当前认证数据(只读)
  186. */
  187. export function getCurrentData() {
  188. return { ...currentData };
  189. }
  190. // 登录相关
  191. // ========================================
  192. /**
  193. <<<<<<< HEAD
  194. * 执行登录请求并保存 token
  195. * @param {string} user 用户名
  196. * @param {string} pass 密码
  197. */
  198. async function doLogin(user, pass) {
  199. const res = await axiosInstance.post('/auth?rember=true', {
  200. method: 'key',
  201. key: `${user}@${md5(pass)}`,
  202. });
  203. currentData.token = {
  204. authName: res.data.authName,
  205. authKey: res.data.authKey,
  206. };
  207. if (res.data.refreshToken)
  208. currentData.refreshToken = res.data.refreshToken;
  209. await saveCurrentData();
  210. }
  211. /**
  212. * 交互式登录,循环直至登录成功或用户取消
  213. */
  214. async function interactiveLogin() {
  215. while (true) {
  216. try {
  217. const user = await input({ message: '输入用户名' });
  218. const pass = await password({ message: '输入密码' });
  219. await doLogin(user, pass);
  220. consola.success('登录成功');
  221. return;
  222. } catch (e) {
  223. if (e?.name === 'ExitPromptError' || e?.name === 'CancelPromptError')
  224. return;
  225. logError('登录失败', getErrorMessage(e));
  226. }
  227. }
  228. }
  229. /**
  230. =======
  231. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  232. * 检查登录状态
  233. */
  234. export async function checkLogged() {
  235. try {
  236. await axiosInstance.get('/auth');
  237. <<<<<<< HEAD
  238. consola.success('已登录');
  239. } catch (e) {
  240. logError('获取状态失败:', getErrorMessage(e));
  241. =======
  242. console.log('已登录');
  243. } catch (e) {
  244. console.error('获取状态失败:', getErrorMessage(e));
  245. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  246. }
  247. }
  248. /**
  249. * 登录
  250. * @param {string} user 用户名
  251. */
  252. export async function login(user) {
  253. try {
  254. <<<<<<< HEAD
  255. const username = user || await input({ message: '输入用户名' });
  256. const pass = await password({ message: '输入密码' });
  257. await doLogin(username, pass);
  258. consola.success('登录成功');
  259. } catch (e) {
  260. logError('登录失败', getErrorMessage(e));
  261. =======
  262. const pass = await password({ message: '输入密码' });
  263. const res = await axiosInstance.post('/auth?rember=true', {
  264. method: 'key',
  265. key: `${user}@${md5(pass)}`,
  266. });
  267. currentData.token = {
  268. authName: res.data.authName,
  269. authKey: res.data.authKey,
  270. };
  271. await writeFile(constant.TokenSave, JSON.stringify(currentData));
  272. console.log('登录成功');
  273. } catch (e) {
  274. console.error('登录失败', getErrorMessage(e));
  275. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  276. }
  277. }
  278. /**
  279. * 退出登录
  280. */
  281. export async function logout() {
  282. currentData.token = '';
  283. <<<<<<< HEAD
  284. currentData.refreshToken = '';
  285. await saveCurrentData();
  286. try {
  287. await axiosInstance.delete('/auth');
  288. consola.success('退出登录成功');
  289. } catch (e) {
  290. logError('退出登录失败', getErrorMessage(e));
  291. =======
  292. await writeFile(constant.TokenSave, JSON.stringify(currentData));
  293. try {
  294. await axiosInstance.delete('/auth');
  295. console.log('退出登录成功');
  296. } catch (e) {
  297. console.error('退出登录失败', getErrorMessage(e));
  298. >>>>>>> 7c11b917b224c1bfd3d86d6cc15ea83fede0098f
  299. }
  300. }