feat: add query-login

This commit is contained in:
2025-03-22 00:52:58 +08:00
parent 3d45a83129
commit 4b16ec8499
17 changed files with 530 additions and 650 deletions

View File

@@ -32,7 +32,7 @@ const downloadAppCommand = new Command('download')
data.id = id;
}
const res = await queryApp(data);
let registry = 'https://kevisual.xiongxiao.me';
let registry = 'https://kevisual.cn';
if (options?.registry) {
registry = new URL(options.registry).origin;
}

View File

@@ -1,2 +1 @@
import './micro-app/index.ts';
import './front-app/index.ts';

View File

@@ -1,122 +0,0 @@
/**
* 下载 app serve client的包的命令
*/
import { chalk } from '@/module/chalk.ts';
import { program, Command } from '../../../program.ts';
import fs from 'fs';
import { Readable } from 'stream';
import * as tar from 'tar';
import path from 'path';
import { fileIsExist } from '@/uitls/file.ts';
// Utility function to convert a web ReadableStream to a Node.js Readable stream
function nodeReadableStreamFromWeb(webStream: ReadableStream<Uint8Array>) {
const reader = webStream.getReader();
return new Readable({
async read() {
const { done, value } = await reader.read();
if (done) {
this.push(null);
} else {
this.push(Buffer.from(value));
}
},
});
}
export const appCommand = new Command('micro-app').description('micro-app 命令').action(() => {
console.log('micro-app');
});
program.addCommand(appCommand);
// https://kevisual.xiongxiao.me/api/micro-app/download/file?notNeedToken=y&title=mark-0.0.2.tgz
const downloadAppCommand = new Command('download')
.description('下载 app serve client的包. \nmicro-app download -i mark-0.0.2.tgz -o test/mark.tgz -x test2')
.option('-i, --id <id>', '下载 app serve client的包, id 或者title, mark-0.0.2.tgz')
.option('-o, --output <output>', '下载 app serve client的包, 输出路径')
.option('-x, --extract <extract>', '下载 app serve client的包, 解压, 默认解压到当前目录')
.option('-r, --registry <registry>', '下载 app serve client的包, 使用私有源')
.action(async (options) => {
const id = options.id || '';
if (!id) {
console.error(chalk.red('id is required'));
return;
}
let title = '';
if (id.includes('.tgz')) {
title = id;
}
let registry = '';
if (options?.registry) {
registry = new URL(options.registry).origin;
} else {
registry = 'https://kevisual.xiongxiao.me';
}
let curlUrl = `${registry}/api/micro-app/download/${id}?notNeedToken=y`;
if (title) {
curlUrl = `${registry}/api/micro-app/download/file?notNeedToken=y&title=${title}`;
}
console.log(chalk.blue('下载地址:'), curlUrl);
fetch(curlUrl)
.then(async (res) => {
const contentDisposition = res.headers.get('content-disposition');
let filename = ''; // Default filename
if (contentDisposition) {
const match = contentDisposition.match(/filename="?(.+)"?/);
if (match && match[1]) {
filename = match[1].replace(/^"|"$/g, '');
}
}
if (!filename) {
console.log(chalk.red('下载失败: 没有找到下载文件, 请检查下载地址是否正确,或手动下载'));
return;
}
const outputPath = options.output || filename;
if (!fileIsExist(outputPath)) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
}
const fileStream = fs.createWriteStream(outputPath);
if (res.body) {
nodeReadableStreamFromWeb(res.body).pipe(fileStream);
fileStream.on('finish', async () => {
console.log(chalk.green(`下载成功: ${outputPath}`));
if (options.extract) {
console.log(chalk.green(`解压: ${outputPath}`));
const extractPath = path.join(process.cwd(), options.extract || '.');
if (!fileIsExist(extractPath)) {
fs.mkdirSync(extractPath, { recursive: true });
}
const fileInput = path.join(process.cwd(), outputPath);
tar
.extract({
file: fileInput,
cwd: extractPath,
})
.then((res) => {
console.log(chalk.green(`解压成功: ${outputPath}`));
})
.catch((err) => {
console.error(chalk.red(`解压失败: ${outputPath}, 请手动解压, tar -xvf ${outputPath}`));
});
}
});
fileStream.on('error', (err) => {
console.error(chalk.red('文件写入错误:', err));
fileStream.close(); // Ensure the stream is closed on error
});
} else {
console.error(chalk.red('下载失败: 无法获取文件流'));
}
})
.catch((err) => {
console.error(chalk.red('下载请求失败:', err));
});
});
appCommand.addCommand(downloadAppCommand);

View File

@@ -3,13 +3,12 @@ import glob from 'fast-glob';
import path from 'path';
import fs from 'fs';
import FormData from 'form-data';
import { getBaseURL, query } from '@/module/query.ts';
import { getBaseURL, query, storage } from '@/module/query.ts';
import { getConfig } from '@/module/index.ts';
import inquirer from 'inquirer';
import { packLib, unpackLib } from './publish.ts';
import chalk from 'chalk';
import { installDeps } from '@/uitls/npm.ts';
const command = new Command('deploy')
.description('把前端文件传到服务器')
.argument('<filePath>', 'Path to the file to be uploaded, filepath or directory') // 定义文件路径参数
@@ -123,10 +122,11 @@ const uploadFiles = async (
if (username) {
form.append('username', username);
}
return new Promise((resolve) => {
return new Promise(async (resolve) => {
const _baseURL = getBaseURL();
const url = new URL(_baseURL);
console.log('upload url', url.hostname, url.protocol, url.port);
const token = await storage.getItem('token');
form.submit(
{
path: '/api/app/upload',
@@ -135,7 +135,7 @@ const uploadFiles = async (
port: url.port,
method: 'POST',
headers: {
Authorization: 'Bearer ' + config.token,
Authorization: 'Bearer ' + token,
...form.getHeaders(),
},
},

View File

@@ -1,49 +1,12 @@
import { program, Command } from '@/program.ts';
import { getConfig, writeConfig } from '@/module/get-config.ts';
import { getBaseURL } from '@/module/query.ts';
import { queryLogin, queryMe, switchOrg, switchMe } from '@/query/index.ts';
import { getConfig } from '@/module/get-config.ts';
import inquirer from 'inquirer';
import { runApp } from '../app-run.ts';
import { chalk } from '@/module/chalk.ts';
import { loginInCommand } from '@/module/login/login-by-web.ts';
export const saveToken = async (token: string) => {
const baseURL = getBaseURL();
const config = getConfig();
writeConfig({ ...config, token });
const res = await runApp({ path: 'config', key: 'saveToken', payload: { baseURL, token } });
if (res.code !== 200) {
console.log('Set token failed', res.message || '');
}
};
export const switchToken = async (baseURL: string) => {
const res = await runApp({ path: 'config', key: 'switchToken', payload: { baseURL } });
if (res.code !== 200 && res.code !== 404) {
console.log('switch token failed', res.message || '');
}
return res;
};
export const deleteToken = async (baseURL: string) => {
const res = await runApp({ path: 'config', key: 'deleteToken', payload: { baseURL } });
if (res.code !== 200) {
console.log('delete token failed', res.message || '');
}
return res;
};
export const getTokenList = async () => {
const res = await runApp({ path: 'config', key: 'getTokenList' });
if (res.code !== 200) {
console.log('get token list failed', res.message || '');
}
return res;
};
export const setTokenList = async (data: any[]) => {
const res = await runApp({ path: 'config', key: 'setTokenList', payload: { data } });
if (res.code !== 200) {
console.log('set token list failed', res.message || '');
}
return res;
};
// 定义login命令支持 `-u` 和 `-p` 参数来输入用户名和密码
import { queryLogin, storage } from '@/module/query.ts';
/**
* 定义login命令支持 `-u` 和 `-p` 参数来输入用户名和密码
*/
const loginCommand = new Command('login')
.description('Login to the application')
.option('-u, --username <username>', 'Specify username')
@@ -51,10 +14,9 @@ const loginCommand = new Command('login')
.option('-f, --force', 'Force login')
.option('-w, --web', 'Login on the web')
.action(async (options) => {
const config = getConfig();
let { username, password } = options;
if (options.web) {
await loginInCommand(saveToken);
await loginInCommand();
return;
}
// 如果没有传递参数,则通过交互式输入
@@ -77,7 +39,8 @@ const loginCommand = new Command('login')
username = answers.username || username;
password = answers.password || password;
}
if (config.token) {
const token = storage.getItem('token');
if (token) {
const res = await showMe(false);
if (res.code === 200) {
const data = res.data;
@@ -88,11 +51,11 @@ const loginCommand = new Command('login')
}
}
const res = await queryLogin(username, password);
const res = await queryLogin.login({
username,
password,
});
if (res.code === 200) {
const { token } = res.data;
writeConfig({ ...config, token });
saveToken(token);
console.log('welcome', username);
} else {
console.log('登录失败', res.message || '');
@@ -102,47 +65,19 @@ const loginCommand = new Command('login')
program.addCommand(loginCommand);
const showMe = async (show = true) => {
const me = await queryMe();
const me = await queryLogin.getMe();
if (show) {
// save me to config
const meSet = await runApp({ path: 'config', key: 'meSet', payload: { data: me.data } });
if (me.code === 200) {
console.log('Me', me.data);
} else {
const config = getConfig();
console.log('Show Me failed', me.message);
writeConfig({ ...config, token: '' });
}
console.log('Me', me.data);
}
return me;
};
const switchOrgCommand = new Command('switch').argument('<username>', 'Switch to another organization or username').action(async (username) => {
const config = getConfig();
if (!config.token) {
console.log('Please login first');
return;
}
const meGet = await runApp({ path: 'config', key: 'meGet' });
if (meGet.code !== 200) {
console.log('Please login first');
return;
}
const me = meGet.data?.value || {};
if (me?.username === username) {
// console.log('Already in', options);
console.log('success switch to', username);
return;
}
const res = await switchOrg(username);
const res = await queryLogin.switchUser(username);
if (res.code === 200) {
const token = res.data.token;
writeConfig({ ...config, token });
console.log(`Switch ${username} Success`);
saveToken(token);
await showMe();
console.log('success switch to', username);
} else {
console.log(`Switch ${username} Failed`, res.message || '');
console.log('switch to', username, 'failed', res.message || '');
}
});

View File

@@ -1,42 +1,19 @@
import { program as app, Command } from '@/program.ts';
import { getConfig, query, writeConfig } from '@/module/index.ts';
import { getConfig, writeConfig } from '@/module/index.ts';
import { queryLogin, storage } from '@/module/query.ts';
import inquirer from 'inquirer';
import util from 'util';
import { saveToken, switchToken, deleteToken, getTokenList, setTokenList } from './login.ts';
const token = new Command('token').description('show token').action(async () => {
const config = getConfig();
console.log('token', config.token);
const token = storage.getItem('token');
console.log('token', token);
});
const tokenList = new Command('list')
.description('show token list')
.option('-r --remove <number>', 'remove token by number')
// .option('-r --remove <number>', 'remove token by number')
.action(async (opts) => {
const res = await getTokenList();
if (res.code !== 200) {
console.error('get token list failed', res.message || '');
return;
}
console.log(util.inspect(res.data.value, { colors: true, depth: 4 }));
const list = res.data.value || [];
if (opts.remove) {
const index = Number(opts.remove) - 1;
if (index < 0 || index >= list.length) {
console.log('index out of range');
return;
}
const removeBase = list.splice(index, 1);
const baseURL = removeBase[0];
if (baseURL.baseURL) {
const res = await deleteToken(baseURL?.baseURL);
if (res.code !== 200) {
return;
}
}
console.log('delete token success', 'delete', baseURL);
return;
}
const res = queryLogin.cache.cache.cacheData;
console.log(util.inspect(res, { colors: true, depth: 4 }));
});
token.addCommand(tokenList);
app.addCommand(token);
@@ -101,7 +78,7 @@ const baseURL = new Command('baseURL')
list = quineList(list);
showList(list);
writeConfig({ ...config, baseURLList: list });
removeBase[0] && deleteToken(removeBase[0]);
removeBase[0];
return;
}
if (opts.set) {
@@ -119,7 +96,6 @@ const baseURL = new Command('baseURL')
} else {
baseURL = opts.set;
}
baseURL && switchToken(baseURL);
return;
}
if (opts.list) {
@@ -128,12 +104,11 @@ const baseURL = new Command('baseURL')
}
if (opts.clear) {
writeConfig({ ...config, baseURLList: [] });
setTokenList([]);
return;
}
if (!config.baseURL) {
config = getConfig();
writeConfig({ ...config, baseURL: 'https://kevisual.xiongxiao.me' });
writeConfig({ ...config, baseURL: 'https://kevisual.cn' });
config = getConfig();
}
console.log('current baseURL:', config.baseURL);
@@ -159,7 +134,6 @@ const setBaseURL = new Command('set')
baseURL = answers.baseURL;
}
writeConfig({ ...config, baseURL });
baseURL && switchToken(baseURL);
});
baseURL.addCommand(setBaseURL);

View File

@@ -122,9 +122,8 @@ const npmrc = new Command('set')
const npmrcContent =
config?.npmrc ||
`//npm.xiongxiao.me/:_authToken=\${ME_NPM_TOKEN}
@abearxiong:registry=https://npm.pkg.github.com
//registry.npmjs.org/:_authToken=\${NPM_TOKEN}
@kevisual:registry=https://npm.xiongxiao.me`;
`;
const execPath = process.cwd();
const npmrcPath = path.resolve(execPath, '.npmrc');
let writeFlag = false;

View File

@@ -1,46 +1,26 @@
import MD5 from 'crypto-js/md5.js';
import { getBaseURL, query } from '../query.ts';
import { getBaseURL, queryLogin } from '../query.ts';
import { chalk } from '../chalk.ts';
import jsonwebtoken from 'jsonwebtoken';
import { BaseLoad } from '@kevisual/load';
import { getConfig, writeConfig } from '../get-config.ts';
type LoginWithWebOptions = {};
export const loginWithWeb = async (opts?: LoginWithWebOptions) => {
const baseURL = getBaseURL();
const randomId = Math.random().toString(36).substring(2, 15);
const timestamp = Date.now();
const tokenSecret = 'xiao' + randomId;
const sign = MD5(`${tokenSecret}${timestamp}`).toString();
const token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, {
// 10分钟过期
expiresIn: 60 * 10, // 10分钟
});
const config = await getConfig();
const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`;
console.log(chalk.blue(url));
return {
url,
token,
tokenSecret,
};
const res = queryLogin.loginWithWeb(baseURL, { MD5, jsonwebtoken });
console.log(chalk.blue(res.url));
return res;
};
type PollLoginOptions = {
tokenSecret: string;
saveToken?: any;
};
export const pollLoginStatus = async (token: string, opts: PollLoginOptions) => {
const load = new BaseLoad();
load.load(
async () => {
const res = await query.post({
path: 'user',
key: 'checkLoginStatus',
loginToken: token,
});
const res = await queryLogin.checkLoginStatus(token);
return res;
},
{
@@ -55,36 +35,21 @@ export const pollLoginStatus = async (token: string, opts: PollLoginOptions) =>
timeout: 60 * 3 * 1000, // 5分钟超时
});
if (res.code === 200 && res.data?.code === 200) {
const data = res.data?.data;
try {
const payload = jsonwebtoken.verify(data, opts.tokenSecret) as UserPayload;
type UserPayload = {
userToken: {
token: string;
expireTime: number;
};
user: {
id: string;
username: string;
};
};
const userToken = payload.userToken;
// console.log('token:\n\n', userToken);
console.log(chalk.green('网页登录成功', payload?.user?.username));
console.log(chalk.green('token:', userToken.token));
await opts?.saveToken(userToken.token);
console.log(chalk.green('网页登录成功'));
return;
} catch (error) {
console.log(chalk.red('登录失败'), error);
return;
}
}
console.log(chalk.red('登录失败'), res);
};
export const loginInCommand = async (saveToken: any) => {
const { url, token, tokenSecret } = await loginWithWeb();
await pollLoginStatus(token, { tokenSecret, saveToken });
return url;
export const loginInCommand = async () => {
const baseURL = getBaseURL();
const res = queryLogin.loginWithWeb(baseURL, { MD5, jsonwebtoken });
console.log(chalk.blue(res.url));
await pollLoginStatus(res.token, { tokenSecret: res.tokenSecret });
return res.url;
};
// loginInCommand();

View File

@@ -1,7 +1,9 @@
import { Query } from '@kevisual/query/query';
import { getConfig } from './get-config.ts';
import { QueryLoginNode, storage } from '@kevisual/query-login/node';
const config = getConfig();
export const baseURL = config?.baseURL || 'https://envision.xiongxiao.me';
export const baseURL = config?.baseURL || 'https://kevisual.cn';
export { storage };
export const getBaseURL = () => {
if (typeof config?.dev === 'undefined') {
return baseURL;
@@ -23,10 +25,29 @@ export const query = new Query({
query.beforeRequest = async (config) => {
if (config.headers) {
const token = await getConfig()?.token;
const token = await storage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
}
return config;
};
query.afterResponse = async (response, ctx) => {
if (response.code === 401) {
if (query.stop) {
return {
code: 500,
message: '登录已过期',
};
}
query.stop = true;
const res = await queryLogin.afterCheck401ToRefreshToken(response, ctx);
query.stop = false;
return res;
}
return response as any;
};
export const queryLogin = new QueryLoginNode({
query: query as any,
onLoad: async () => {},
});

View File

@@ -1,208 +0,0 @@
import { app } from '@/app.ts';
import { Config } from './model/config.ts';
import { getConfig, writeConfig } from '@/module/get-config.ts';
import { queryMe } from '@/query/index.ts';
const cacheToken = 'tokenList';
export type TokenCacheItem = {
baseURL?: string;
token?: string;
expireTime?: number;
};
app
.route({
path: 'config',
key: 'getTokenList',
description: 'Get token list',
})
.define(async (ctx) => {
const tokenList = await Config.findOne({
where: {
key: cacheToken,
},
logging: false,
});
if (tokenList) {
ctx.body = tokenList;
return;
}
ctx.body = {
value: [],
};
})
.addTo(app);
app
.route({
path: 'config',
key: 'setTokenList',
description: 'Set token list',
})
.define(async (ctx) => {
const { data } = ctx.query;
if (!data) {
ctx.throw(400, 'data is required');
}
let config = await Config.findOne({
where: { key: cacheToken }, // 自定义条件
logging: false,
});
if (!config) {
config = await Config.create(
{
key: cacheToken,
value: data,
},
{ logging: false },
);
ctx.body = config;
return;
} else {
config.value = data;
await config.save();
ctx.body = config;
}
})
.addTo(app);
app
.route({
path: 'config',
key: 'clearToken',
description: 'Clear token list',
})
.define(async (ctx) => {
const config = await Config.findOne({
where: { key: cacheToken },
logging: false,
});
if (config) {
await config.destroy();
}
ctx.body = 'success';
})
.addTo(app);
app
.route({
path: 'config',
key: 'saveToken',
description: 'Add token',
validator: {
baseURL: {
type: 'string',
required: true,
message: 'baseURL is required',
},
token: {
type: 'string',
required: true,
message: 'token is required',
},
},
})
.define(async (ctx) => {
const { baseURL, token } = ctx.query;
if (!baseURL || !token) {
ctx.throw(400, 'baseURL and token are required');
}
const data: TokenCacheItem = {
baseURL,
token,
};
const tokenRes = await ctx.call({ path: 'config', key: 'getTokenList' });
if (tokenRes.code !== 200) {
ctx.throw(tokenRes.code, tokenRes.message || 'Failed to get token list');
}
const tokenList: TokenCacheItem[] = tokenRes.body?.value || [];
// Check if the token already exists
const index = tokenList.findIndex((item) => item.baseURL === data.baseURL);
if (index > -1) {
tokenList[index] = data;
} else {
tokenList.push(data);
}
const res = await ctx.call({ path: 'config', key: 'setTokenList', payload: { data: tokenList } });
if (res.code === 200) {
ctx.body = res.body?.value;
} else ctx.throw(res.code, res.message || 'Failed to add token');
})
.addTo(app);
app
.route({
path: 'config',
key: 'switchToken',
description: 'Switch token user',
validator: {
baseURL: {
type: 'string',
required: true,
message: 'baseURL is required',
},
},
})
.define(async (ctx) => {
const { baseURL } = ctx.query;
const configRes = await ctx.call({ path: 'config', key: 'getTokenList' });
if (configRes.code !== 200) {
ctx.throw(configRes.code, configRes.message || 'Failed to get token list');
}
const tokenList: TokenCacheItem[] = configRes.body?.value || [];
const index = tokenList.findIndex((item) => item.baseURL === baseURL);
const token = index > -1 ? tokenList[index].token : '';
if (token) {
const config = getConfig();
const resMe = await queryMe();
if (resMe.code !== 200) {
writeConfig({ ...config, token: '' });
ctx.throw(resMe.code, resMe.message || 'cache token is invalid');
}
writeConfig({ ...config, token: token });
ctx.body = {
baseURL: baseURL,
token: tokenList[index].token,
};
} else {
writeConfig({ ...getConfig(), token: '' });
ctx.throw(404, 'Token not found');
}
})
.addTo(app);
app
.route({
path: 'config',
key: 'deleteToken',
description: 'Delete token',
validator: {
baseURL: {
type: 'string',
required: true,
message: 'baseURL is required',
},
},
})
.define(async (ctx) => {
const { baseURL } = ctx.query;
const config = await ctx.call({ path: 'config', key: 'getTokenList' });
if (config.code !== 200) {
ctx.throw(config.code, config.message || 'Failed to get token list');
}
const tokenList: TokenCacheItem[] = config.body?.value || [];
const index = tokenList.findIndex((item) => item.baseURL === baseURL);
if (index > -1) {
tokenList.splice(index, 1);
const res = await ctx.call({ path: 'config', key: 'setTokenList', payload: { data: tokenList } });
if (res.code === 200) {
ctx.body = res.body;
} else ctx.throw(res.code, res.message || 'Failed to delete token');
} else {
console.log('not has token', baseURL);
ctx.body = {
value: tokenList,
};
}
})
.addTo(app);

View File

@@ -1,2 +1 @@
import './list.ts'
import './cache-token.ts'
import './list.ts'

View File

@@ -1,53 +0,0 @@
import { getConfig } from '../module/get-config.ts';
import { runApp } from '../app-run.ts';
const getConfigList = async () => {
const res = await runApp({
path: 'config',
key: 'getTokenList',
});
console.log(res);
};
// getConfigList();
const setConfigList = async () => {
const config = getConfig();
const { baseURL, token } = config;
console.log(baseURL, token);
const res = await runApp({
path: 'config',
key: 'saveToken',
payload: {
baseURL: 'abc32',
token,
},
});
console.log(res);
};
// setConfigList();
const switchToken = async () => {
const res = await runApp({
path: 'config',
key: 'switchToken',
payload: {
baseURL: 'abc2',
},
});
console.log(res);
};
// switchToken();
const removeToken = async () => {
const res = await runApp({
path: 'config',
key: 'deleteToken',
payload: {
baseURL: 'abc32',
},
});
console.log(res);
};
removeToken();