diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b247dc2 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +password=888888 +envision_registry=https://kevisual.silkyai.cn/api/router \ No newline at end of file diff --git a/.gitignore b/.gitignore index 843f34a..c277352 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist node_modules -.DS_Store \ No newline at end of file +.DS_Store +.env \ No newline at end of file diff --git a/package.json b/package.json index b7dbb76..dbedde1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/query-login", - "version": "0.0.1", + "version": "0.0.2", "description": "", "main": "dist/query-login.js", "types": "dist/query-login.d.ts", @@ -23,9 +23,13 @@ "tsup": "^8.4.0" }, "exports": { - ".": "./dist/query-login.js" + ".": "./dist/query-login-browser.js", + "./base": "./dist/query-login.js", + "./node": "./dist/query-login-node.js", + "./browser": "./dist/query-login-browser.js" }, "dependencies": { - "@kevisual/cache": "^0.0.1" + "@kevisual/cache": "^0.0.1", + "dotenv": "^16.4.7" } } \ No newline at end of file diff --git a/src/login-cache.ts b/src/login-cache.ts index a6da843..bfedb61 100644 --- a/src/login-cache.ts +++ b/src/login-cache.ts @@ -1,4 +1,17 @@ -import { MyCache } from '@kevisual/cache'; +export interface Cache { + /** + * @update 获取缓存 + */ + get(key: string): Promise; + /** + * @update 设置缓存 + */ + set(key: string, value: any): Promise; + /** + * @update 删除缓存 + */ + del(): Promise; +} export type CacheLoginUser = { user?: any; @@ -10,22 +23,14 @@ type CacheLogin = { loginUsers: CacheLoginUser[]; } & CacheLoginUser; -export interface CacheStore { +export type CacheStore = { name: string; cacheData: CacheLogin; + /** + * 实际操作的cache + */ cache: T; - /** - * @update 获取缓存 - */ - get(key: string): Promise; - /** - * @update 设置缓存 - */ - set(key: string, value: CacheLogin): Promise; - /** - * @update 删除缓存 - */ - del(): Promise; + /** * 设置当前用户 */ @@ -58,13 +63,22 @@ export interface CacheStore { * 清除所有用户 */ clearAll(): Promise; -} -export class LoginCacheStore implements CacheStore> { - cache: MyCache; +} & Cache; + +type LoginCacheStoreOpts = { + name: string; + cache: Cache; +}; +export class LoginCacheStore implements CacheStore { + cache: Cache; name: string; cacheData: CacheLogin; - constructor(name: string) { - this.cache = new MyCache(name); + constructor(opts: LoginCacheStoreOpts) { + if (!opts.cache) { + throw new Error('cache is required'); + } + // @ts-ignore + this.cache = opts.cache; this.cacheData = { loginUsers: [], user: undefined, @@ -72,7 +86,7 @@ export class LoginCacheStore implements CacheStore> { accessToken: undefined, refreshToken: undefined, }; - this.name = name; + this.name = opts.name; } /** * 设置缓存 diff --git a/src/login-node-cache.ts b/src/login-node-cache.ts new file mode 100644 index 0000000..f10ebee --- /dev/null +++ b/src/login-node-cache.ts @@ -0,0 +1,112 @@ +import { Cache } from './login-cache.ts'; +import { homedir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { readFile, writeFile, unlink, stat, mkdir } from 'node:fs/promises'; +export const fileExists = async (filePath: string, createIfNotExists = false) => { + try { + await stat(filePath); + return true; + } catch (error) { + if (createIfNotExists) { + await mkdir(dirname(filePath), { recursive: true }); + return false; + } + return false; + } +}; +export const readConfigFile = (filePath: string) => { + try { + const data = readFileSync(filePath, 'utf-8'); + const jsonData = JSON.parse(data); + return jsonData; + } catch (error) { + return {}; + } +}; +export const getHostName = () => { + const configDir = join(homedir(), '.config', 'envision'); + const configFile = join(configDir, 'config.json'); + const config = readConfigFile(configFile); + const baseURL = config.baseURL || 'https://kevisual.cn'; + const hostname = new URL(baseURL).hostname; + return hostname; +}; +export class StorageNode implements Storage { + cacheData: any; + filePath: string; + constructor() { + this.cacheData = {}; + const configDir = join(homedir(), '.config', 'envision'); + const hostname = getHostName(); + this.filePath = join(configDir, 'config', `${hostname}-storage.json`); + fileExists(this.filePath, true); + } + async loadCache() { + const filePath = this.filePath; + try { + const data = await readFile(filePath, 'utf-8'); + const jsonData = JSON.parse(data); + this.cacheData = jsonData; + } catch (error) { + this.cacheData = {}; + await writeFile(filePath, JSON.stringify(this.cacheData, null, 2)); + } + } + get length() { + return Object.keys(this.cacheData).length; + } + getItem(key: string) { + return this.cacheData[key]; + } + setItem(key: string, value: any) { + this.cacheData[key] = value; + writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2)); + } + removeItem(key: string) { + delete this.cacheData[key]; + writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2)); + } + clear() { + this.cacheData = {}; + writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2)); + } + key(index: number) { + return Object.keys(this.cacheData)[index]; + } +} +export class LoginNodeCache implements Cache { + name: string; + cacheData: any; + filePath: string; + constructor(name: string) { + this.name = name; + const hostname = getHostName(); + this.filePath = join(homedir(), '.config', 'envision', 'config', `${hostname}-${name}.json`); + fileExists(this.filePath, true); + this.loadCache(this.filePath); + } + + async loadCache(filePath: string) { + try { + const data = await readFile(filePath, 'utf-8'); + const jsonData = JSON.parse(data); + this.cacheData = jsonData; + } catch (error) { + const defaultData = { loginUsers: [] }; + this.cacheData = defaultData; + await writeFile(filePath, JSON.stringify(defaultData, null, 2)); + } + } + + async get() { + return this.cacheData; + } + async set(key: string, value: any) { + this.cacheData = value; + await writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2)); + } + async del() { + await unlink(this.filePath); + } +} diff --git a/src/query-login-browser.ts b/src/query-login-browser.ts new file mode 100644 index 0000000..2d131cb --- /dev/null +++ b/src/query-login-browser.ts @@ -0,0 +1,12 @@ +import { QueryLogin, QueryLoginOpts } from './query-login.ts'; +import { MyCache } from '@kevisual/cache'; +type QueryLoginNodeOptsWithoutCache = Omit; + +export class QueryLoginBrowser extends QueryLogin { + constructor(opts: QueryLoginNodeOptsWithoutCache) { + super({ + ...opts, + cache: new MyCache('login'), + }); + } +} diff --git a/src/query-login-node.ts b/src/query-login-node.ts new file mode 100644 index 0000000..d90cabb --- /dev/null +++ b/src/query-login-node.ts @@ -0,0 +1,14 @@ +import { QueryLogin, QueryLoginOpts } from './query-login.ts'; +import { LoginNodeCache, StorageNode } from './login-node-cache.ts'; +type QueryLoginNodeOptsWithoutCache = Omit; +export const storage = new StorageNode(); +await storage.loadCache(); +export class QueryLoginNode extends QueryLogin { + constructor(opts: QueryLoginNodeOptsWithoutCache) { + super({ + ...opts, + storage, + cache: new LoginNodeCache('login'), + }); + } +} diff --git a/src/query-login.ts b/src/query-login.ts index 4459dbe..828ce3d 100644 --- a/src/query-login.ts +++ b/src/query-login.ts @@ -2,12 +2,14 @@ import { Query } from '@kevisual/query'; import type { Result, DataOpts } from '@kevisual/query/query'; import { setBaseResponse } from '@kevisual/query/query'; import { LoginCacheStore, CacheStore } from './login-cache.ts'; +import { Cache } from './login-cache.ts'; -type QueryLoginOpts = { +export type QueryLoginOpts = { query?: Query; isBrowser?: boolean; onLoad?: () => void; storage?: Storage; + cache: Cache; }; export type QueryLoginData = { username?: string; @@ -21,6 +23,9 @@ export type QueryLoginResult = { export class QueryLogin { query: Query; + /** + * query login cache, 非实际操作, 一个cache的包裹模块 + */ cache: CacheStore; isBrowser: boolean; load?: boolean; @@ -29,7 +34,7 @@ export class QueryLogin { constructor(opts?: QueryLoginOpts) { this.query = opts?.query || new Query(); - this.cache = new LoginCacheStore('login'); + this.cache = new LoginCacheStore({ name: 'login', cache: opts.cache }); this.isBrowser = opts?.isBrowser ?? true; this.init(); this.onLoad = opts?.onLoad; @@ -257,4 +262,38 @@ export class QueryLogin { }, ); } + /** + * 检查登录状态 + * @param token + * @returns + */ + async checkLoginStatus(token: string) { + const res = await this.post({ + path: 'user', + key: 'checkLoginStatus', + loginToken: token, + }); + if (res.code === 200) { + const accessToken = res.data?.accessToken; + this.storage.setItem('token', accessToken || ''); + await this.beforeSetLoginUser({ accessToken, refreshToken: res.data?.refreshToken }); + return res; + } + return false; + } + /** + * 使用web登录,创建url地址, 需要MD5和jsonwebtoken + */ + loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5: any; jsonwebtoken: any }) { + 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 url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`; + return { url, token, tokenSecret }; + } } diff --git a/src/test-node/login.ts b/src/test-node/login.ts new file mode 100644 index 0000000..7a74344 --- /dev/null +++ b/src/test-node/login.ts @@ -0,0 +1,58 @@ +import { QueryLoginNode, storage } from '../query-login-node.ts'; +import { Query } from '@kevisual/query'; +import dotenv from 'dotenv'; +dotenv.config(); + +const { password, envision_registry } = process.env; +const query = new Query({ + url: envision_registry, +}); +query.before(async (options) => { + const token = storage.getItem('token'); + if (token) { + options.headers = { + ...options.headers, + Authorization: `Bearer ${token}`, + }; + } + return options; +}); +const queryLogin = new QueryLoginNode({ + query, + isBrowser: false, +}); + +const login = async () => { + const res = await queryLogin + .login({ + username: 'root', + password: password, + }) + .then((res) => { + console.log(res); + }); + return res; +}; + +// login(); + +const getMe = async () => { + const res = await queryLogin.getMe(); + console.log(res); +}; +getMe(); + +// console.log('token', storage.getItem('token')); + +const switchUser = async (username: string) => { + const res = await queryLogin.switchUser(username); + console.log(res); +}; +// switchUser('admin'); + +// switchUser('root'); +const logout = async () => { + const res = await queryLogin.logout(); + console.log(res); +}; +// logout(); diff --git a/src/test/login.ts b/src/test/login.ts index 283a7c6..63554c4 100644 --- a/src/test/login.ts +++ b/src/test/login.ts @@ -1,4 +1,4 @@ -import { QueryLogin } from '../query-login.ts'; +import { QueryLoginBrowser } from '../query-login-browser.ts'; import { Query } from '@kevisual/query'; const query = new Query({ url: 'https://kevisual.silkyai.cn/api/router', @@ -14,7 +14,7 @@ query.before(async (options) => { } return options; }); -const queryLogin = new QueryLogin({ +const queryLogin = new QueryLoginBrowser({ query, isBrowser: true, }); diff --git a/tsup.config.ts b/tsup.config.ts index 94872f4..c4d50f9 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: ['src/query-login.ts'], + entry: ['src/query-login.ts', 'src/query-login-browser.ts', 'src/query-login-node.ts'], splitting: false, sourcemap: false, @@ -10,4 +10,7 @@ export default defineConfig({ dts: true, outDir: 'dist', tsconfig: 'tsconfig.json', + define: { + IS_BROWSER: 'false', + }, });