Skip to content

Commit

Permalink
wip: 青龙面板更新,新增支持以应用密钥的方式认证
Browse files Browse the repository at this point in the history
  • Loading branch information
renxia committed Apr 12, 2024
1 parent be44993 commit a69cdd8
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 35 deletions.
6 changes: 1 addition & 5 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm exec flh --tscheck --only-changes
pnpm exec flh --prettier --cache --fix
pnpm test
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"url": "https://lzw.me"
},
"scripts": {
"prepare": "husky install",
"prepare": "husky || true",
"dev": "tsc -p tsconfig.cjs.json -w",
"lint": "flh --prettier --tscheck",
"build": "npm run clean && tsc -p tsconfig.cjs.json",
Expand Down Expand Up @@ -45,17 +45,17 @@
"./template"
],
"dependencies": {
"@lzwme/fe-utils": "^1.7.0",
"@lzwme/fe-utils": "^1.7.2",
"micromatch": "^4.0.5"
},
"devDependencies": {
"@lzwme/fed-lint-helper": "^2.5.2",
"@types/node": "^20.11.22",
"@types/node": "^20.12.3",
"base64-js": "^1.5.1",
"crypto-js": "^4.2.0",
"husky": "^9.0.11",
"prettier": "^3.2.5",
"standard-version": "^9.5.0",
"typescript": "^5.3.3"
"typescript": "^5.4.3"
}
}
44 changes: 37 additions & 7 deletions src/lib/QingLong.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,33 @@
* @Author: renxia
* @Date: 2024-01-18 10:12:52
* @LastEditors: renxia
* @LastEditTime: 2024-02-05 22:52:18
* @LastEditTime: 2024-03-27 09:46:52
* @Description: 青龙面板 API 简易封装
*/
import { existsSync } from 'node:fs';
import { Request, readJsonFileSync, color } from '@lzwme/fe-utils';
import { Request, readJsonFileSync, color, TOTP } from '@lzwme/fe-utils';
import { logger } from './helper';
import { TOTP } from './TOTP';

export class QingLoing {
private static inc: QingLoing;
static getInstance(config?: QLOptions) {
if (!QingLoing.inc) QingLoing.inc = new QingLoing(config);
return QingLoing.inc;
}
private isOpenApi = false;
private loginStatus: number = 0;
private req = new Request('', { 'content-type': 'application/json' });
public async request<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', url: string, params?: any): Promise<QLResponse<T>> {
if (!url.startsWith('http')) url = this.config.host + (url.startsWith('/') ? '' : '/') + url;
if (this.isOpenApi) url = url.replace('/api/', '/open/');
const { data } = await this.req.request<QLResponse<T>>(method, url, params);

if (data.code === 401 && this.loginStatus === 1) {
this.loginStatus = 0;
this.config.token = '';
if (await this.login()) return this.request(params, url, params);
}
logger.debug(`[QL][req][${color.cyan(method)}]`, color.gray(url), '\n res:', data, '\n req:', params);
if (this.config.debug) logger.info(`[QL][req][${color.cyan(method)}]`, color.gray(url), '\n req:', params, '\n res:', data);
return data;
}
constructor(private config: QLOptions = {}) {}
Expand All @@ -39,6 +40,7 @@ export class QingLoing {
if (!this.config.host) this.config.host = 'http://127.0.0.1:5700';
if (this.config.host.endsWith('/')) this.config.host = this.config.host.slice(0, -1);

// 尝试从配置文件中读取
for (const filepath of ['/ql/data/config/auth.json', '/ql/config/auth.json']) {
if (existsSync(filepath)) {
const r = readJsonFileSync<{ token: string; username: string; password: string; twoFactorSecret: string }>(filepath);
Expand All @@ -49,16 +51,28 @@ export class QingLoing {
}
}

if (config.clientId && config.clientSecret) {
const data = await this.request<{ token: string }>(
'GET',
`/open/auth/token?client_id=${config.clientId}&client_secret=${config.clientSecret}`
);
if (data.data?.token) {
config.token = data.data.token;
this.isOpenApi = true;
logger.log(`[QL]OpenApi 获取 token 成功!`);
} else logger.error(`[QL]OpenApi 获取 token 异常: ${data.message}`);
}

if (!config.token && !config.username) {
this.loginStatus = -100;
logger.debug('未设置青龙面板相关相关信息');
if (this.config.debug) logger.info('未设置青龙面板相关相关信息');
return false;
}

// 验证 token 有效性
if (this.config.token) {
this.req.setHeaders({ Authorization: `Bearer ${this.config.token}` });
const r = await this.request('GET', `/api/user?t=${Date.now()}`);
const r = await this.request<QLEnvItem[]>('GET', `/api/envs?searchValue=&t=${Date.now()}`);
if (r.code !== 200) {
logger.warn('token 已失效:', r);
this.config.token = '';
Expand Down Expand Up @@ -141,7 +155,23 @@ export class QingLoing {
}
}

export type QLOptions = { host?: string; token?: string; username?: string; password?: string; twoFactorSecret?: string };
export type QLOptions = {
debug?: boolean;
/** 青龙服务地址。用于上传环境变量,若设置为空则不上传 */
host?: string;
/** 青龙服务 token。用于创建或更新 QL 环境变量配置。会自动尝试从 /ql/data/config/auth.json 文件中获取 */
token?: string;
/** 登录用户名 */
username?: string;
/** 登录密码 */
password?: string;
/** 两步验证秘钥。若开启了两步验证则需设置 */
twoFactorSecret?: string;
/** open app client_id: 应用设置-创建应用,权限选择 环境变量 */
clientId?: string;
/** open app client_secret */
clientSecret?: string;
};
export type QLEnvItem = { name: string; value: string; id?: string; remarks: string };
export type QLResponse<T = any> = { code: number; message: string; data: T };
export interface QLTaskItem {
Expand Down
49 changes: 31 additions & 18 deletions src/lib/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @Author: renxia
* @Date: 2024-01-11 13:38:34
* @LastEditors: renxia
* @LastEditTime: 2024-03-22 10:44:22
* @LastEditTime: 2024-04-12 09:31:07
* @Description:
*/
import fs from 'node:fs';
Expand All @@ -16,12 +16,13 @@ import { QingLoing, type QLResponse, type QLEnvItem } from './QingLong';
const { green, magenta, gray, cyan } = color;
const updateCache = { qlEnvList: [] as QLEnvItem[], updateTime: 0 };

export async function updateToQlEnvConfig({ name, value, desc }: EnvConfig, updateEnvValue?: RuleItem['updateEnvValue']) {
export async function updateToQlEnvConfig(envConfig: EnvConfig, updateEnvValue?: RuleItem['updateEnvValue']) {
const config = getConfig();
const ql = QingLoing.getInstance(config.ql);
const ql = QingLoing.getInstance({ ...config.ql, debug: config.debug });
if (!(await ql.login())) return;

if (Date.now() - updateCache.updateTime > 1000 * 60 * 60 * 1) updateCache.qlEnvList = [];
let { name, value, desc } = envConfig;
let item = updateCache.qlEnvList.find(d => d.name === name);
if (!item) {
updateCache.qlEnvList = await ql.getEnvList();
Expand All @@ -40,12 +41,17 @@ export async function updateToQlEnvConfig({ name, value, desc }: EnvConfig, upda
}

if (updateEnvValue) {
if (updateEnvValue instanceof RegExp) params.value = updateEnvValueByRegExp(updateEnvValue, { name, value, desc }, item.value);
if (updateEnvValue instanceof RegExp) params.value = updateEnvValueByRegExp(updateEnvValue, envConfig, item.value);
else params.value = await updateEnvValue({ name, value }, item.value, X);
if (!params.value) return;
} else if (value.includes('##') && item.value.includes('##')) {
// 支持配置以 ## 隔离 uid
params.value = updateEnvValueByRegExp(/##([a-z0-9_\-*]+)/i, { name, value, desc }, item.value);
params.value = updateEnvValueByRegExp(/##([a-z0-9_\-*]+)/i, envConfig, item.value);
}

if (!params.value) return;

if (params.value.length + 10 < item.value.length) {
logger.warn(`[QL]更新值长度小于原始值!\nOLD: ${item.value}\nNEW: ${params.value}`);
}

params.id = item.id;
Expand All @@ -62,9 +68,10 @@ export async function updateToQlEnvConfig({ name, value, desc }: EnvConfig, upda
return value;
}

export async function updateEnvConfigFile({ name, value, desc }: EnvConfig, updateEnvValue: RuleItem['updateEnvValue'], filePath: string) {
export async function updateEnvConfigFile(envConfig: EnvConfig, updateEnvValue: RuleItem['updateEnvValue'], filePath: string) {
if (!filePath) return;

let { name, value, desc } = envConfig;
let content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
const isExist = content.includes(`export ${name}=`);

Expand All @@ -77,12 +84,12 @@ export async function updateEnvConfigFile({ name, value, desc }: EnvConfig, upda
}

if (updateEnvValue) {
if (updateEnvValue instanceof RegExp) value = updateEnvValueByRegExp(updateEnvValue, { name, value }, oldValue);
else value = (await updateEnvValue({ name, value }, oldValue, X)) as string;
if (updateEnvValue instanceof RegExp) value = updateEnvValueByRegExp(updateEnvValue, envConfig, oldValue);
else value = (await updateEnvValue(envConfig, oldValue, X)) as string;
if (!value) return;
} else if (value.includes('##') && value.includes('##')) {
// 支持配置以 ## 隔离 uid
value = updateEnvValueByRegExp(/##([a-z0-9_\-*]+)/i, { name, value, desc }, value);
value = updateEnvValueByRegExp(/##([a-z0-9_\-*]+)/i, envConfig, value);
}

content = content.replace(new RegExp(`export ${name}=.*`, 'g'), `export ${name}="${value}"`);
Expand All @@ -96,24 +103,30 @@ export async function updateEnvConfigFile({ name, value, desc }: EnvConfig, upda
}

/** 更新处理已存在的环境变量,返回合并后的结果。若无需修改则可返回空 */
function updateEnvValueByRegExp(re: RegExp, { name, value }: EnvConfig, oldValue: string) {
function updateEnvValueByRegExp(re: RegExp, { name, value, sep }: EnvConfig, oldValue: string) {
if (!(re instanceof RegExp)) throw Error(`[${name}]updateEnvValue 应为一个正则匹配表达式`);

const sep = oldValue.includes('\n') ? '\n' : '&';
if (sep !== '&') value = value.replaceAll('&', sep);
const sepList = ['\n', '&'];
const oldSep = sep || sepList.find(d => oldValue.includes(d));
const curSep = sep || sepList.find(d => value.includes(d));
if (!sep) sep = oldSep || curSep || '\n';
// if (sep !== '&') value = value.replaceAll('&', sep);

const val: string[] = [];
const values = value.split(sep).map(d => [d, d.match(re)?.[0]]);
const values = value.split(curSep || sep).map(d => ({ value: d, id: d.match(re)?.[0] }));

oldValue.split(sep).forEach(cookie => {
oldValue.split(oldSep || sep).forEach(cookie => {
const uidValue = cookie.match(re)?.[0];
if (uidValue) {
const item = values.find(d => d[1] === uidValue);
val.push(item?.[0] || cookie);
const item = values.find(d => d.id === uidValue);
val.push(item ? item.value : cookie);
} else {
logger.warn(`[${name}][updateEnvValueByRegExp]oldValue未匹配到uid`, re, cookie);
val.push(cookie);
}
});

values.forEach(d => !val.includes(d[0]) && val.push(d[0]));
values.forEach(d => !val.includes(d.value) && val.push(d.value));

return val.join(sep);
}
9 changes: 8 additions & 1 deletion typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @Author: renxia
* @Date: 2024-01-11 16:53:50
* @LastEditors: renxia
* @LastEditTime: 2024-03-01 15:57:52
* @LastEditTime: 2024-04-12 09:17:15
* @Description:
*/
/// <reference path="global.d.ts" />
Expand Down Expand Up @@ -31,6 +31,10 @@ export interface W2XScriptsConfig {
password?: string;
/** 两步验证秘钥。若开启了两步验证则需设置 */
twoFactorSecret?: string;
/** open app client_id: 应用设置-创建应用,权限选择 环境变量 */
clientId?: string;
/** open app client_secret */
clientSecret?: string;
};
/** 写入环境变量信息到本地文件的路径。若设置为空则不写入 */
envConfFile?: string;
Expand Down Expand Up @@ -184,7 +188,10 @@ export interface EnvConfig {
name?: string;
/** 环境变量值 */
value: string;
/** 描述信息 */
desc?: string;
/** 多账号分隔符 */
sep?: string;
}

interface CacheData<T = any> {
Expand Down

0 comments on commit a69cdd8

Please sign in to comment.