Compare commits
	
		
			11 Commits
		
	
	
		
			0f0a4cbd6a
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9dfb3f4160 | |||
| f694299059 | |||
| 922b0c421f | |||
| 83f65e1554 | |||
| 2547355964 | |||
| ad0d2e717f | |||
| 0a72db7771 | |||
| 162d4c72b4 | |||
| 60c5a986ed | |||
| 0c36328ac3 | |||
| 1cce7e1193 | 
							
								
								
									
										52
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@kevisual/code-center-module", |   "name": "@kevisual/code-center-module", | ||||||
|   "version": "0.0.14", |   "version": "0.0.24", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "main": "dist/system.mjs", |   "main": "dist/system.mjs", | ||||||
|   "module": "dist/system.mjs", |   "module": "dist/system.mjs", | ||||||
| @@ -25,52 +25,45 @@ | |||||||
|   "publishConfig": { |   "publishConfig": { | ||||||
|     "access": "public" |     "access": "public" | ||||||
|   }, |   }, | ||||||
|   "peerDependencies": { |  | ||||||
|     "@kevisual/auth": "^1.0.5", |  | ||||||
|     "@kevisual/router": "^0.0.7", |  | ||||||
|     "@kevisual/use-config": "^1.0.8", |  | ||||||
|     "ioredis": "^5.5.0", |  | ||||||
|     "pg": "^8.13.3", |  | ||||||
|     "sequelize": "^6.37.5" |  | ||||||
|   }, |  | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@kevisual/auth": "1.0.5", |     "@kevisual/auth": "1.0.5", | ||||||
|     "@kevisual/router": "^0.0.9", |     "@kevisual/router": "^0.0.23", | ||||||
|     "@kevisual/use-config": "^1.0.10", |     "@kevisual/use-config": "^1.0.19", | ||||||
|     "ioredis": "^5.6.0", |     "ioredis": "^5.6.1", | ||||||
|     "nanoid": "^5.1.5", |     "nanoid": "^5.1.5", | ||||||
|     "pg": "^8.14.1", |     "pg": "^8.16.2", | ||||||
|     "sequelize": "^6.37.6", |     "sequelize": "^6.37.7", | ||||||
|     "socket.io": "^4.8.1", |     "socket.io": "^4.8.1", | ||||||
|     "zod": "^3.24.2" |     "zod": "^3.25.67" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@kevisual/types": "^0.0.6", |     "@kevisual/context": "^0.0.3", | ||||||
|  |     "@kevisual/types": "^0.0.10", | ||||||
|     "@rollup/plugin-alias": "^5.1.1", |     "@rollup/plugin-alias": "^5.1.1", | ||||||
|     "@rollup/plugin-commonjs": "^28.0.3", |     "@rollup/plugin-commonjs": "^28.0.6", | ||||||
|     "@rollup/plugin-json": "^6.1.0", |     "@rollup/plugin-json": "^6.1.0", | ||||||
|     "@rollup/plugin-node-resolve": "^16.0.1", |     "@rollup/plugin-node-resolve": "^16.0.1", | ||||||
|     "@rollup/plugin-replace": "^6.0.2", |     "@rollup/plugin-replace": "^6.0.2", | ||||||
|     "@rollup/plugin-typescript": "^12.1.2", |     "@rollup/plugin-typescript": "^12.1.3", | ||||||
|     "@types/archiver": "^6.0.3", |     "@types/archiver": "^6.0.3", | ||||||
|     "@types/crypto-js": "^4.2.2", |     "@types/crypto-js": "^4.2.2", | ||||||
|     "@types/formidable": "^3.4.5", |     "@types/formidable": "^3.4.5", | ||||||
|     "@types/jsonwebtoken": "^9.0.9", |     "@types/jsonwebtoken": "^9.0.10", | ||||||
|     "@types/lodash-es": "^4.17.12", |     "@types/lodash-es": "^4.17.12", | ||||||
|     "@types/node": "^22.13.11", |     "@types/node": "^24.0.4", | ||||||
|     "@types/react": "^19.0.12", |     "@types/react": "^19.1.8", | ||||||
|     "@types/uuid": "^10.0.0", |     "@types/uuid": "^10.0.0", | ||||||
|     "concurrently": "^9.1.2", |     "concurrently": "^9.2.0", | ||||||
|     "cross-env": "^7.0.3", |     "cross-env": "^7.0.3", | ||||||
|     "nodemon": "^3.1.9", |     "nodemon": "^3.1.10", | ||||||
|     "rimraf": "^6.0.1", |     "rimraf": "^6.0.1", | ||||||
|     "rollup": "^4.36.0", |     "rollup": "^4.44.1", | ||||||
|     "rollup-plugin-copy": "^3.5.0", |     "rollup-plugin-copy": "^3.5.0", | ||||||
|     "rollup-plugin-dts": "^6.2.1", |     "rollup-plugin-dts": "^6.2.1", | ||||||
|     "rollup-plugin-esbuild": "^6.2.1", |     "rollup-plugin-esbuild": "^6.2.1", | ||||||
|     "tape": "^5.9.0", |     "tape": "^5.9.0", | ||||||
|     "tsx": "^4.19.3", |     "tsx": "^4.20.3", | ||||||
|     "typescript": "^5.8.2" |     "typescript": "^5.8.3" | ||||||
|   }, |   }, | ||||||
|   "exports": { |   "exports": { | ||||||
|     ".": { |     ".": { | ||||||
| @@ -88,6 +81,13 @@ | |||||||
|     "./oauth": { |     "./oauth": { | ||||||
|       "import": "./dist/oauth.mjs", |       "import": "./dist/oauth.mjs", | ||||||
|       "types": "./dist/oauth.d.ts" |       "types": "./dist/oauth.d.ts" | ||||||
|  |     }, | ||||||
|  |     "./mark-model": { | ||||||
|  |       "import": "./dist/mark-model.mjs", | ||||||
|  |       "types": "./dist/mark-model.d.ts" | ||||||
|  |     }, | ||||||
|  |     "./src/*": { | ||||||
|  |       "import": "./src/*" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										694
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										694
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -186,5 +186,46 @@ const oauthConfig = [ | |||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
| ] | ] | ||||||
|  | const markModelConfig = [ | ||||||
| export default [config, dtsConfig, ...systemConfig, ...modelConfig, ...oauthConfig]; |   { | ||||||
|  |     input: './src/mark/mark-model.ts', | ||||||
|  |     output: { | ||||||
|  |       dir: './dist', | ||||||
|  |       entryFileNames: 'mark-model.js', | ||||||
|  |       format: 'esm', | ||||||
|  |     }, | ||||||
|  |     external: [ | ||||||
|  |       ...external, // 引入外部依赖 | ||||||
|  |     ], | ||||||
|  |     plugins: [ | ||||||
|  |       replace(replaceConfig), | ||||||
|  |       alias({ | ||||||
|  |         entries: [ | ||||||
|  |           { find: '@', replacement: path.resolve('src') }, // 配置 @ 为 src 目录 | ||||||
|  |         ], | ||||||
|  |       }), | ||||||
|  |       resolve({ | ||||||
|  |         preferBuiltins: true, // 强制优先使用内置模块 | ||||||
|  |       }), | ||||||
|  |       commonjs(), | ||||||
|  |       esbuild({ | ||||||
|  |         target: 'node22', // 目标为 Node.js 14 | ||||||
|  |         minify: false, // 启用代码压缩 | ||||||
|  |         tsconfig: 'tsconfig.json', | ||||||
|  |       }), | ||||||
|  |       json(), | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |       input: './src/mark/mark-model.ts', | ||||||
|  |     output: { | ||||||
|  |       dir: './dist', | ||||||
|  |       entryFileNames: 'mark-model.d.ts', | ||||||
|  |       format: 'esm', | ||||||
|  |     }, | ||||||
|  |     plugins: [ | ||||||
|  |       dts(), | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | ] | ||||||
|  | export default [config, dtsConfig, ...systemConfig, ...modelConfig, ...oauthConfig, ...markModelConfig]; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { App } from '@kevisual/router'; | import { App } from '@kevisual/router'; | ||||||
| import { useContextKey, useContext } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/context'; | ||||||
|  |  | ||||||
| const init = () => { | const init = (): App => { | ||||||
|   return new App({ |   return new App({ | ||||||
|     serverOptions: { |     serverOptions: { | ||||||
|       cors: { |       cors: { | ||||||
| @@ -10,4 +10,4 @@ const init = () => { | |||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| export const app = useContextKey('app', init); | export const app: App = useContextKey('app', init); | ||||||
|   | |||||||
| @@ -3,8 +3,11 @@ | |||||||
|  */ |  */ | ||||||
| import { UserServices, User, UserInit, UserModel } from './models/user.ts'; | import { UserServices, User, UserInit, UserModel } from './models/user.ts'; | ||||||
| import { Org, OrgInit, OrgModel } from './models/org.ts'; | import { Org, OrgInit, OrgModel } from './models/org.ts'; | ||||||
|  | import { UserSecret, UserSecretInit } from './models/user-secret.ts'; | ||||||
| import { addAuth } from './middleware/auth.ts'; | import { addAuth } from './middleware/auth.ts'; | ||||||
| export { User, Org, UserServices, UserInit, OrgInit, UserModel, OrgModel }; | import { checkAuth, getLoginUser } from './middleware/auth-manual.ts'; | ||||||
|  | export { User, Org, UserServices, UserInit, OrgInit, UserModel, OrgModel, UserSecret, UserSecretInit }; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 可以不需要user成功, 有则赋值,交给下一个中间件 |  * 可以不需要user成功, 有则赋值,交给下一个中间件 | ||||||
|  */ |  */ | ||||||
| @@ -13,4 +16,5 @@ export const authCan = 'auth-can'; | |||||||
|  * 必须需要user成功 |  * 必须需要user成功 | ||||||
|  */ |  */ | ||||||
| export const auth = 'auth'; | export const auth = 'auth'; | ||||||
| export { addAuth }; |  | ||||||
|  | export { addAuth, checkAuth, getLoginUser }; | ||||||
|   | |||||||
| @@ -4,11 +4,13 @@ | |||||||
| import { app } from './app.ts'; | import { app } from './app.ts'; | ||||||
| import { UserServices, UserInit, UserModel, User } from './models/user.ts'; | import { UserServices, UserInit, UserModel, User } from './models/user.ts'; | ||||||
| import { Org, OrgInit, OrgModel } from './models/org.ts'; | import { Org, OrgInit, OrgModel } from './models/org.ts'; | ||||||
|  | import { UserSecret, UserSecretInit } from './models/user-secret.ts'; | ||||||
| import { useContextKey } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/use-config/context'; | ||||||
| import { Sequelize } from 'sequelize'; | import { Sequelize } from 'sequelize'; | ||||||
| import { Redis } from 'ioredis'; | import { Redis } from 'ioredis'; | ||||||
| export { User, UserServices, UserInit, UserModel }; | export { User, UserServices, UserInit, UserModel }; | ||||||
| export { Org, OrgInit, OrgModel }; | export { Org, OrgInit, OrgModel }; | ||||||
|  | export { UserSecret, UserSecretInit }; | ||||||
|  |  | ||||||
| export const redis = useContextKey<Redis>('redis'); | export const redis = useContextKey<Redis>('redis'); | ||||||
| export const sequelize = useContextKey<Sequelize>('sequelize'); | export const sequelize = useContextKey<Sequelize>('sequelize'); | ||||||
| @@ -16,4 +18,5 @@ export { app }; | |||||||
| export const init = () => { | export const init = () => { | ||||||
|   OrgInit(); |   OrgInit(); | ||||||
|   UserInit(); |   UserInit(); | ||||||
|  |   UserSecretInit(); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										327
									
								
								src/mark/mark-model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								src/mark/mark-model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,327 @@ | |||||||
|  | import { useContextKey } from '@kevisual/context'; | ||||||
|  | import { nanoid, customAlphabet } from 'nanoid'; | ||||||
|  | import { DataTypes, Model, ModelAttributes } from 'sequelize'; | ||||||
|  | import type { Sequelize } from 'sequelize'; | ||||||
|  | export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); | ||||||
|  | export type Mark = Partial<InstanceType<typeof MarkModel>>; | ||||||
|  | export type MarkData = { | ||||||
|  |   md?: string; // markdown | ||||||
|  |   mdList?: string[]; // markdown list | ||||||
|  |   type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file | ||||||
|  |   data?: any; | ||||||
|  |   key?: string; // 文件的名称, 唯一 | ||||||
|  |   push?: boolean; // 是否推送到elasticsearch | ||||||
|  |   pushTime?: Date; // 推送时间 | ||||||
|  |   summary?: string; // 摘要 | ||||||
|  |   nodes?: MarkDataNode[]; // 节点 | ||||||
|  |   [key: string]: any; | ||||||
|  | }; | ||||||
|  | export type MarkFile = { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   url: string; | ||||||
|  |   size: number; | ||||||
|  |   type: 'self' | 'data' | 'generate'; // generate为生成文件 | ||||||
|  |   query: string; // 'data.nodes[id].content'; | ||||||
|  |   hash: string; | ||||||
|  |   fileKey: string; // 文件的名称, 唯一 | ||||||
|  | }; | ||||||
|  | export type MarkDataNode = { | ||||||
|  |   id?: string; | ||||||
|  |   [key: string]: any; | ||||||
|  | }; | ||||||
|  | export type MarkConfig = { | ||||||
|  |   [key: string]: any; | ||||||
|  | }; | ||||||
|  | export type MarkAuth = { | ||||||
|  |   [key: string]: any; | ||||||
|  | }; | ||||||
|  | /** | ||||||
|  |  * 隐秘内容 | ||||||
|  |  * auth | ||||||
|  |  * config | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | export class MarkModel extends Model { | ||||||
|  |   declare id: string; | ||||||
|  |   declare title: string; // 标题,可以ai生成 | ||||||
|  |   declare description: string; // 描述,可以ai生成 | ||||||
|  |   declare cover: string; // 封面,可以ai生成 | ||||||
|  |   declare thumbnail: string; // 缩略图 | ||||||
|  |   declare key: string; // 文件路径 | ||||||
|  |   declare markType: string; // markdown | json | html | image | video | audio | code | link | file | ||||||
|  |   declare link: string; // 访问链接 | ||||||
|  |   declare tags: string[]; // 标签 | ||||||
|  |   declare summary: string; // 摘要, description的简化版 | ||||||
|  |   declare data: MarkData; // 数据 | ||||||
|  |  | ||||||
|  |   declare uid: string; // 操作用户的id | ||||||
|  |   declare puid: string; // 父级用户的id, 真实用户 | ||||||
|  |   declare config: MarkConfig; // mark属于一定不会暴露的内容。 | ||||||
|  |  | ||||||
|  |   declare fileList: MarkFile[]; // 文件管理 | ||||||
|  |   declare uname: string; // 用户的名称, 或者着别名 | ||||||
|  |  | ||||||
|  |   declare markedAt: Date; // 标记时间 | ||||||
|  |   declare createdAt: Date; | ||||||
|  |   declare updatedAt: Date; | ||||||
|  |   declare version: number; | ||||||
|  |   /** | ||||||
|  |    * 加锁更新data中的node的节点,通过node的id | ||||||
|  |    * @param param0 | ||||||
|  |    */ | ||||||
|  |   static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) { | ||||||
|  |     const sequelize = opts?.sequelize || (await useContextKey('sequelize')); | ||||||
|  |     const transaction = await sequelize.transaction(); // 开启事务 | ||||||
|  |     const operate = opts.operate || 'update'; | ||||||
|  |     const isUpdate = operate === 'update'; | ||||||
|  |     const Model = opts.Model || MarkModel; | ||||||
|  |     try { | ||||||
|  |       // 1. 获取当前的 JSONB 字段值(加锁) | ||||||
|  |       const mark = await Model.findByPk(id, { | ||||||
|  |         transaction, | ||||||
|  |         lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 | ||||||
|  |       }); | ||||||
|  |       if (!mark) { | ||||||
|  |         throw new Error('Mark not found'); | ||||||
|  |       } | ||||||
|  |       // 2. 修改特定的数组元素 | ||||||
|  |       const data = mark.data as MarkData; | ||||||
|  |       const items = data.nodes; | ||||||
|  |       if (!node.id) { | ||||||
|  |         node.id = random(12); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // 找到要更新的元素 | ||||||
|  |       const itemIndex = items.findIndex((item) => item.id === node.id); | ||||||
|  |       if (itemIndex === -1) { | ||||||
|  |         isUpdate && items.push(node); | ||||||
|  |       } else { | ||||||
|  |         if (isUpdate) { | ||||||
|  |           items[itemIndex] = node; | ||||||
|  |         } else { | ||||||
|  |           items.splice(itemIndex, 1); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       const version = Number(mark.version) + 1; | ||||||
|  |       // 4. 更新 JSONB 字段 | ||||||
|  |       const result = await mark.update( | ||||||
|  |         { | ||||||
|  |           data: { | ||||||
|  |             ...data, | ||||||
|  |             nodes: items, | ||||||
|  |           }, | ||||||
|  |           version, | ||||||
|  |         }, | ||||||
|  |         { transaction }, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await transaction.commit(); | ||||||
|  |       return result; | ||||||
|  |     } catch (error) { | ||||||
|  |       await transaction.rollback(); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) { | ||||||
|  |     const sequelize = opts?.sequelize || (await useContextKey('sequelize')); | ||||||
|  |     const transaction = await sequelize.transaction(); // 开启事务 | ||||||
|  |     const Model = opts?.Model || MarkModel; | ||||||
|  |     try { | ||||||
|  |       const mark = await Model.findByPk(id, { | ||||||
|  |         transaction, | ||||||
|  |         lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 | ||||||
|  |       }); | ||||||
|  |       if (!mark) { | ||||||
|  |         throw new Error('Mark not found'); | ||||||
|  |       } | ||||||
|  |       const data = mark.data as MarkData; | ||||||
|  |       const _nodes = data.nodes || []; | ||||||
|  |       // 过滤不在nodes中的节点 | ||||||
|  |       const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node); | ||||||
|  |       // 更新或删除节点 | ||||||
|  |       const newNodes = _nodes | ||||||
|  |         .map((node) => { | ||||||
|  |           const nodeOperate = nodes.find((n) => n.node.id === node.id); | ||||||
|  |           if (nodeOperate) { | ||||||
|  |             if (nodeOperate.operate === 'delete') { | ||||||
|  |               return null; | ||||||
|  |             } | ||||||
|  |             return nodeOperate.node; | ||||||
|  |           } | ||||||
|  |           return node; | ||||||
|  |         }) | ||||||
|  |         .filter((node) => node !== null); | ||||||
|  |       const version = Number(mark.version) + 1; | ||||||
|  |       const result = await mark.update( | ||||||
|  |         { | ||||||
|  |           data: { | ||||||
|  |             ...data, | ||||||
|  |             nodes: [...blankNodes, ...newNodes], | ||||||
|  |           }, | ||||||
|  |           version, | ||||||
|  |         }, | ||||||
|  |         { transaction }, | ||||||
|  |       ); | ||||||
|  |       await transaction.commit(); | ||||||
|  |       return result; | ||||||
|  |     } catch (error) { | ||||||
|  |       await transaction.rollback(); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) { | ||||||
|  |     const sequelize = opts.sequelize || (await useContextKey('sequelize')); | ||||||
|  |     const transaction = await sequelize.transaction(); // 开启事务 | ||||||
|  |     const Model = opts.Model || MarkModel; | ||||||
|  |     const mark = await Model.findByPk(id, { | ||||||
|  |       transaction, | ||||||
|  |       lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改 | ||||||
|  |     }); | ||||||
|  |     if (!mark) { | ||||||
|  |       throw new Error('Mark not found'); | ||||||
|  |     } | ||||||
|  |     const version = Number(mark.version) + 1; | ||||||
|  |     const result = await mark.update( | ||||||
|  |       { | ||||||
|  |         ...mark.data, | ||||||
|  |         ...data, | ||||||
|  |         data: { | ||||||
|  |           ...mark.data, | ||||||
|  |           ...data, | ||||||
|  |         }, | ||||||
|  |         version, | ||||||
|  |       }, | ||||||
|  |       { transaction }, | ||||||
|  |     ); | ||||||
|  |     await transaction.commit(); | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  |   static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) { | ||||||
|  |     const sequelize = opts.sequelize || (await useContextKey('sequelize')); | ||||||
|  |     const transaction = await sequelize.transaction(); // 开启事务 | ||||||
|  |     const Model = opts.Model || MarkModel; | ||||||
|  |     const result = await Model.create({ ...data, version: 1 }, { transaction }); | ||||||
|  |     await transaction.commit(); | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | export type MarkInitOpts<T = any> = { | ||||||
|  |   tableName: string; | ||||||
|  |   sequelize?: Sequelize; | ||||||
|  |   callInit?: (attribute: ModelAttributes) => ModelAttributes; | ||||||
|  |   Model?: T extends typeof MarkModel ? T : typeof MarkModel; | ||||||
|  | }; | ||||||
|  | export type Opts = { | ||||||
|  |   sync?: boolean; | ||||||
|  |   alter?: boolean; | ||||||
|  |   logging?: boolean | ((...args: any) => any); | ||||||
|  |   force?: boolean; | ||||||
|  | }; | ||||||
|  | export const MarkMInit = async <T = any>(opts: MarkInitOpts<T>, sync?: Opts) => { | ||||||
|  |   const sequelize = await useContextKey('sequelize'); | ||||||
|  |   opts.sequelize = opts.sequelize || sequelize; | ||||||
|  |   const { callInit, Model, ...optsRest } = opts; | ||||||
|  |   const modelAttribute = { | ||||||
|  |     id: { | ||||||
|  |       type: DataTypes.UUID, | ||||||
|  |       primaryKey: true, | ||||||
|  |       defaultValue: DataTypes.UUIDV4, | ||||||
|  |       comment: 'id', | ||||||
|  |     }, | ||||||
|  |     title: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: '', | ||||||
|  |     }, | ||||||
|  |     key: { | ||||||
|  |       type: DataTypes.TEXT, // 对应的minio的文件路径 | ||||||
|  |       defaultValue: '', | ||||||
|  |     }, | ||||||
|  |     markType: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file | ||||||
|  |       comment: '类型', | ||||||
|  |     }, | ||||||
|  |     description: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: '', | ||||||
|  |     }, | ||||||
|  |     cover: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: '', | ||||||
|  |       comment: '封面', | ||||||
|  |     }, | ||||||
|  |     thumbnail: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: '', | ||||||
|  |       comment: '缩略图', | ||||||
|  |     }, | ||||||
|  |     link: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: '', | ||||||
|  |       comment: '链接', | ||||||
|  |     }, | ||||||
|  |     tags: { | ||||||
|  |       type: DataTypes.JSONB, | ||||||
|  |       defaultValue: [], | ||||||
|  |     }, | ||||||
|  |     summary: { | ||||||
|  |       type: DataTypes.TEXT, | ||||||
|  |       defaultValue: '', | ||||||
|  |       comment: '摘要', | ||||||
|  |     }, | ||||||
|  |     config: { | ||||||
|  |       type: DataTypes.JSONB, | ||||||
|  |       defaultValue: {}, | ||||||
|  |     }, | ||||||
|  |     data: { | ||||||
|  |       type: DataTypes.JSONB, | ||||||
|  |       defaultValue: {}, | ||||||
|  |     }, | ||||||
|  |     fileList: { | ||||||
|  |       type: DataTypes.JSONB, | ||||||
|  |       defaultValue: [], | ||||||
|  |     }, | ||||||
|  |     uname: { | ||||||
|  |       type: DataTypes.STRING, | ||||||
|  |       defaultValue: '', | ||||||
|  |       comment: '用户的名称, 更新后的用户的名称', | ||||||
|  |     }, | ||||||
|  |     version: { | ||||||
|  |       type: DataTypes.INTEGER, // 更新刷新版本,多人协作 | ||||||
|  |       defaultValue: 1, | ||||||
|  |     }, | ||||||
|  |     markedAt: { | ||||||
|  |       type: DataTypes.DATE, | ||||||
|  |       allowNull: true, | ||||||
|  |       comment: '标记时间', | ||||||
|  |     }, | ||||||
|  |     uid: { | ||||||
|  |       type: DataTypes.UUID, | ||||||
|  |       allowNull: true, | ||||||
|  |     }, | ||||||
|  |     puid: { | ||||||
|  |       type: DataTypes.UUID, | ||||||
|  |       allowNull: true, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |   const InitModel = Model || MarkModel; | ||||||
|  |   InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, { | ||||||
|  |     sequelize, | ||||||
|  |     paranoid: true, | ||||||
|  |     ...optsRest, | ||||||
|  |   }); | ||||||
|  |   if (sync && sync.sync) { | ||||||
|  |     const { sync: _, ...rest } = sync; | ||||||
|  |     MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => { | ||||||
|  |       console.error('MarkModel sync', e); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const markModelInit = MarkMInit; | ||||||
|  |  | ||||||
|  | export const syncMarkModel = async (sync?: Opts, tableName = 'micro_mark') => { | ||||||
|  |   const sequelize = await useContextKey('sequelize'); | ||||||
|  |   await MarkMInit({ sequelize, tableName }, sync); | ||||||
|  | }; | ||||||
							
								
								
									
										78
									
								
								src/mark/mark.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/mark/mark.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | export type Mark<T = any> = { | ||||||
|  |   /** | ||||||
|  |    * 标记ID | ||||||
|  |    */ | ||||||
|  |   id: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 标题 | ||||||
|  |    */ | ||||||
|  |   title?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 描述 | ||||||
|  |    */ | ||||||
|  |   description?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 标签 | ||||||
|  |    */ | ||||||
|  |   tags?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 标记类型 | ||||||
|  |    */ | ||||||
|  |   markType?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 封面 | ||||||
|  |    */ | ||||||
|  |   cover?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 链接 | ||||||
|  |    */ | ||||||
|  |   link?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 摘要 | ||||||
|  |    */ | ||||||
|  |   summary?: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 键 | ||||||
|  |    */ | ||||||
|  |   key?: string; | ||||||
|  |   data: T; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 附件列表 | ||||||
|  |    */ | ||||||
|  |   fileList?: any[]; | ||||||
|  |   /** | ||||||
|  |    * 创建人信息 | ||||||
|  |    */ | ||||||
|  |   uname?: string; | ||||||
|  |   /** | ||||||
|  |    * 版本号 | ||||||
|  |    */ | ||||||
|  |   version?: number; | ||||||
|  |   /** | ||||||
|  |    * 创建时间 | ||||||
|  |    */ | ||||||
|  |   createdAt: Date; | ||||||
|  |   /** | ||||||
|  |    * 更新时间 | ||||||
|  |    */ | ||||||
|  |   updatedAt: Date; | ||||||
|  |   /** | ||||||
|  |    * 标记时间 | ||||||
|  |    */ | ||||||
|  |   markedAt?: Date; | ||||||
|  |   uid?: string; | ||||||
|  |   puid?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ensureType = ['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file'] | ||||||
|  |  | ||||||
|  | export type MarkEnsureType = typeof ensureType[number]; | ||||||
							
								
								
									
										81
									
								
								src/middleware/auth-manual.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/middleware/auth-manual.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | import { User } from '../models/user.ts'; | ||||||
|  | import http from 'http'; | ||||||
|  | import cookie from 'cookie'; | ||||||
|  | export const error = (msg: string, code = 500) => { | ||||||
|  |   return JSON.stringify({ code, message: msg }); | ||||||
|  | }; | ||||||
|  | type CheckAuthOptions = { | ||||||
|  |   check401?: boolean; // 是否返回权限信息 | ||||||
|  | }; | ||||||
|  | /** | ||||||
|  |  * 手动验证token,如果token不存在,则返回401 | ||||||
|  |  * @param req | ||||||
|  |  * @param res | ||||||
|  |  * @returns | ||||||
|  |  */ | ||||||
|  | export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse, opts?: CheckAuthOptions) => { | ||||||
|  |   let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || ''; | ||||||
|  |   const url = new URL(req.url || '', 'http://localhost'); | ||||||
|  |   const check401 = opts?.check401 ?? true; // 是否返回401错误 | ||||||
|  |   const resNoPermission = () => { | ||||||
|  |     res.statusCode = 401; | ||||||
|  |     res.end(error('Invalid authorization')); | ||||||
|  |     return { tokenUser: null, token: null, hasToken: false }; | ||||||
|  |   }; | ||||||
|  |   if (!token) { | ||||||
|  |     token = url.searchParams.get('token') || ''; | ||||||
|  |   } | ||||||
|  |   if (!token) { | ||||||
|  |     const parsedCookies = cookie.parse(req.headers.cookie || ''); | ||||||
|  |     token = parsedCookies.token || ''; | ||||||
|  |   } | ||||||
|  |   if (!token && check401) { | ||||||
|  |     return resNoPermission(); | ||||||
|  |   } | ||||||
|  |   if (token) { | ||||||
|  |     token = token.replace('Bearer ', ''); | ||||||
|  |   } | ||||||
|  |   let tokenUser; | ||||||
|  |   const hasToken = !!token; // 是否有token存在 | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     tokenUser = await User.verifyToken(token); | ||||||
|  |   } catch (e) { | ||||||
|  |     console.log('checkAuth error', e); | ||||||
|  |     res.statusCode = 401; | ||||||
|  |     res.end(error('Invalid token')); | ||||||
|  |     return { tokenUser: null, token: null, hasToken: false }; | ||||||
|  |   } | ||||||
|  |   return { tokenUser, token, hasToken }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取登录用户,有则获取,无则返回null | ||||||
|  |  * @param req | ||||||
|  |  * @returns | ||||||
|  |  */ | ||||||
|  | export const getLoginUser = async (req: http.IncomingMessage) => { | ||||||
|  |   let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || ''; | ||||||
|  |   const url = new URL(req.url || '', 'http://localhost'); | ||||||
|  |   if (!token) { | ||||||
|  |     token = url.searchParams.get('token') || ''; | ||||||
|  |   } | ||||||
|  |   if (!token) { | ||||||
|  |     const parsedCookies = cookie.parse(req.headers.cookie || ''); | ||||||
|  |     token = parsedCookies.token || ''; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (token) { | ||||||
|  |     token = token.replace('Bearer ', ''); | ||||||
|  |   } | ||||||
|  |   if (!token) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |   let tokenUser; | ||||||
|  |   try { | ||||||
|  |     tokenUser = await User.verifyToken(token); | ||||||
|  |     return { tokenUser, token }; | ||||||
|  |   } catch (e) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { DataTypes, Model, Op, Sequelize } from 'sequelize'; | import { DataTypes, Model, Op, Sequelize } from 'sequelize'; | ||||||
| import { useContextKey } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/context'; | ||||||
| import { SyncOpts, User } from './user.ts'; | import { SyncOpts, User } from './user.ts'; | ||||||
|  |  | ||||||
| type AddUserOpts = { | type AddUserOpts = { | ||||||
|   | |||||||
							
								
								
									
										247
									
								
								src/models/user-secret.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								src/models/user-secret.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,247 @@ | |||||||
|  | import { DataTypes, Model, Sequelize } from 'sequelize'; | ||||||
|  |  | ||||||
|  | import { useContextKey } from '@kevisual/context'; | ||||||
|  | import { Redis } from 'ioredis'; | ||||||
|  | import { SyncOpts, User } from './user.ts'; | ||||||
|  | import { oauth } from '../oauth/auth.ts'; | ||||||
|  | import { OauthUser } from '@/oauth/oauth.ts'; | ||||||
|  | export const redis = useContextKey<Redis>('redis'); | ||||||
|  |  | ||||||
|  | const UserSecretStatus = ['active', 'inactive', 'expired'] as const; | ||||||
|  | export class UserSecret extends Model { | ||||||
|  |   static oauth = oauth; | ||||||
|  |   declare id: string; | ||||||
|  |   declare token: string; | ||||||
|  |   declare userId: string; | ||||||
|  |   declare orgId: string; | ||||||
|  |   declare title: string; | ||||||
|  |   declare description: string; | ||||||
|  |   declare status: (typeof UserSecretStatus)[number]; | ||||||
|  |   declare expiredTime: Date; | ||||||
|  |   declare data: any; | ||||||
|  |   /** | ||||||
|  |    * 验证token | ||||||
|  |    * @param token | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   static async verifyToken(token: string) { | ||||||
|  |     if (!oauth.isSecretKey(token)) { | ||||||
|  |       return await oauth.verifyToken(token); | ||||||
|  |     } | ||||||
|  |     // const secretToken = await oauth.verifyToken(token); | ||||||
|  |     // if (secretToken) { | ||||||
|  |     //   return secretToken; | ||||||
|  |     // } | ||||||
|  |     const userSecret = await UserSecret.findOne({ | ||||||
|  |       where: { token }, | ||||||
|  |     }); | ||||||
|  |     if (!userSecret) { | ||||||
|  |       return null; // 如果没有找到对应的用户密钥,则返回null | ||||||
|  |     } | ||||||
|  |     if (userSecret.isExpired()) { | ||||||
|  |       return null; // 如果用户密钥已过期,则返回null | ||||||
|  |     } | ||||||
|  |     if (userSecret.status !== 'active') { | ||||||
|  |       return null; // 如果用户密钥状态不是active,则返回null | ||||||
|  |     } | ||||||
|  |     if (!userSecret.token) { | ||||||
|  |       return null; // 如果用户密钥没有token,则返回null | ||||||
|  |     } | ||||||
|  |     // 如果用户密钥未过期,则返回用户信息 | ||||||
|  |     const oauthUser = await userSecret.getOauthUser(); | ||||||
|  |     if (!oauthUser) { | ||||||
|  |       return null; // 如果没有找到对应的oauth用户,则返回null | ||||||
|  |     } | ||||||
|  |     // await oauth.saveSecretKey(oauthUser, userSecret.token); | ||||||
|  |     // 存储到oauth中的token store中 | ||||||
|  |     return oauthUser; | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * owner 组织用户的 oauthUser | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async getOauthUser() { | ||||||
|  |     const user = await User.findOne({ | ||||||
|  |       where: { id: this.userId }, | ||||||
|  |       attributes: ['id', 'username', 'type', 'owner'], | ||||||
|  |     }); | ||||||
|  |     let org: User = null; | ||||||
|  |     if (!user) { | ||||||
|  |       return null; // 如果没有找到对应的用户,则返回null | ||||||
|  |     } | ||||||
|  |     const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null; | ||||||
|  |     const oauthUser: Partial<OauthUser> = { | ||||||
|  |       id: user.id, | ||||||
|  |       username: user.username, | ||||||
|  |       type: 'user', | ||||||
|  |       oauthExpand: { | ||||||
|  |         expiredTime: expiredTime, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     if (this.orgId) { | ||||||
|  |       org = await User.findOne({ | ||||||
|  |         where: { id: this.orgId }, | ||||||
|  |         attributes: ['id', 'username', 'type', 'owner'], | ||||||
|  |       }); | ||||||
|  |       if (org) { | ||||||
|  |         oauthUser.id = org.id; | ||||||
|  |         oauthUser.username = org.username; | ||||||
|  |         oauthUser.type = 'org'; | ||||||
|  |         oauthUser.uid = user.id; | ||||||
|  |       } else { | ||||||
|  |         console.warn(`getOauthUser: org not found for orgId ${this.orgId}`); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return oauth.getOauthUser(oauthUser); | ||||||
|  |   } | ||||||
|  |   isExpired() { | ||||||
|  |     if (!this.expiredTime) { | ||||||
|  |       return false; // 没有设置过期时间 | ||||||
|  |     } | ||||||
|  |     const now = Date.now(); | ||||||
|  |     const expiredTime = new Date(this.expiredTime); | ||||||
|  |     return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期 | ||||||
|  |   } | ||||||
|  |   async createNewToken() { | ||||||
|  |     if (this.token) { | ||||||
|  |       await oauth.delToken(this.token); | ||||||
|  |     } | ||||||
|  |     const token = await UserSecret.createToken(); | ||||||
|  |     this.token = token; | ||||||
|  |     await this.save(); | ||||||
|  |     return token; | ||||||
|  |   } | ||||||
|  |   static async createToken() { | ||||||
|  |     let token = oauth.generateSecretKey(); | ||||||
|  |     // 确保生成的token是唯一的 | ||||||
|  |     while (await UserSecret.findOne({ where: { token } })) { | ||||||
|  |       token = oauth.generateSecretKey(); | ||||||
|  |     } | ||||||
|  |     return token; | ||||||
|  |   } | ||||||
|  |   static async createSecret(tokenUser: { id: string; uid?: string }, expireDay = 365) { | ||||||
|  |     const expireTime = expireDay * 24 * 60 * 60 * 1000; // 转换为毫秒 | ||||||
|  |     const token = await UserSecret.createToken(); | ||||||
|  |     let userId = tokenUser.id; | ||||||
|  |     let orgId: string = null; | ||||||
|  |     if (tokenUser.uid) { | ||||||
|  |       userId = tokenUser.uid; | ||||||
|  |       orgId = tokenUser.id; // 如果是组织用户,则uid是组织ID | ||||||
|  |     } | ||||||
|  |     const userSecret = await UserSecret.create({ | ||||||
|  |       userId, | ||||||
|  |       orgId, | ||||||
|  |       token, | ||||||
|  |       expiredTime: new Date(Date.now() + expireTime), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return userSecret; | ||||||
|  |   } | ||||||
|  |   async getPermission(opts: { id: string; uid?: string }) { | ||||||
|  |     const { id, uid } = opts; | ||||||
|  |     let userId: string = id; | ||||||
|  |     let hasPermission = false; | ||||||
|  |     let isUser = false; | ||||||
|  |     let isAdmin: boolean = null; | ||||||
|  |     if (uid) { | ||||||
|  |       userId = uid; | ||||||
|  |     } | ||||||
|  |     if (!id) { | ||||||
|  |       return { | ||||||
|  |         hasPermission, | ||||||
|  |         isUser, | ||||||
|  |         isAdmin, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (this.userId === userId) { | ||||||
|  |       hasPermission = true; | ||||||
|  |       isUser = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (hasPermission) { | ||||||
|  |       return { | ||||||
|  |         hasPermission, | ||||||
|  |         isUser, | ||||||
|  |         isAdmin, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (this.orgId) { | ||||||
|  |       const orgUser = await User.findByPk(this.orgId); | ||||||
|  |       if (orgUser && orgUser.owner === userId) { | ||||||
|  |         isAdmin = true; | ||||||
|  |         hasPermission = true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       hasPermission, | ||||||
|  |       isUser, | ||||||
|  |       isAdmin, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | /** | ||||||
|  |  * 组织模型,在sequelize之后初始化 | ||||||
|  |  */ | ||||||
|  | export const UserSecretInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => { | ||||||
|  |   const sequelize = useContextKey<Sequelize>('sequelize'); | ||||||
|  |   UserSecret.init( | ||||||
|  |     { | ||||||
|  |       id: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         primaryKey: true, | ||||||
|  |         defaultValue: DataTypes.UUIDV4, | ||||||
|  |       }, | ||||||
|  |       description: { | ||||||
|  |         type: DataTypes.TEXT, | ||||||
|  |         allowNull: true, | ||||||
|  |       }, | ||||||
|  |       status: { | ||||||
|  |         type: DataTypes.STRING, | ||||||
|  |         allowNull: true, | ||||||
|  |         defaultValue: 'active', | ||||||
|  |         comment: '状态', | ||||||
|  |       }, | ||||||
|  |       title: { | ||||||
|  |         type: DataTypes.TEXT, | ||||||
|  |         allowNull: true, | ||||||
|  |       }, | ||||||
|  |       expiredTime: { | ||||||
|  |         type: DataTypes.DATE, | ||||||
|  |         allowNull: true, | ||||||
|  |       }, | ||||||
|  |       token: { | ||||||
|  |         type: DataTypes.STRING, | ||||||
|  |         allowNull: false, | ||||||
|  |         comment: '用户密钥', | ||||||
|  |         defaultValue: '', | ||||||
|  |       }, | ||||||
|  |       userId: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         allowNull: true, | ||||||
|  |       }, | ||||||
|  |       data: { | ||||||
|  |         type: DataTypes.JSONB, | ||||||
|  |         allowNull: true, | ||||||
|  |         defaultValue: {}, | ||||||
|  |       }, | ||||||
|  |       orgId: { | ||||||
|  |         type: DataTypes.UUID, | ||||||
|  |         allowNull: true, | ||||||
|  |         comment: '组织ID', | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       sequelize: newSequelize || sequelize, | ||||||
|  |       modelName: tableName || 'cf_user_secret', | ||||||
|  |     }, | ||||||
|  |   ); | ||||||
|  |   if (sync) { | ||||||
|  |     await UserSecret.sync({ alter: true, logging: false, ...sync }).catch((e) => { | ||||||
|  |       console.error('UserSecret sync', e); | ||||||
|  |     }); | ||||||
|  |     return UserSecret; | ||||||
|  |   } | ||||||
|  |   return UserSecret; | ||||||
|  | }; | ||||||
|  | export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret); | ||||||
| @@ -3,13 +3,13 @@ import { nanoid, customAlphabet } from 'nanoid'; | |||||||
| import { CustomError } from '@kevisual/router'; | import { CustomError } from '@kevisual/router'; | ||||||
| import { Org } from './org.ts'; | import { Org } from './org.ts'; | ||||||
|  |  | ||||||
| import { useContextKey } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/context'; | ||||||
| import { Redis } from 'ioredis'; | import { Redis } from 'ioredis'; | ||||||
| import { oauth } from '../oauth/auth.ts'; | import { oauth } from '../oauth/auth.ts'; | ||||||
| import { cryptPwd } from '../oauth/salt.ts'; | import { cryptPwd } from '../oauth/salt.ts'; | ||||||
| import { OauthUser } from '../oauth/oauth.ts'; | import { OauthUser } from '../oauth/oauth.ts'; | ||||||
| export const redis = useContextKey<Redis>('redis'); | export const redis = useContextKey<Redis>('redis'); | ||||||
|  | import { UserSecret } from './user-secret.ts'; | ||||||
| type UserData = { | type UserData = { | ||||||
|   orgs?: string[]; |   orgs?: string[]; | ||||||
|   wxUnionId?: string; |   wxUnionId?: string; | ||||||
| @@ -42,12 +42,13 @@ export class User extends Model { | |||||||
|   setTokenUser(tokenUser: any) { |   setTokenUser(tokenUser: any) { | ||||||
|     this.tokenUser = tokenUser; |     this.tokenUser = tokenUser; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * uid 是用于 orgId 的用户id, 如果uid存在,则表示是用户是组织,其中uid为真实用户 |    * uid 是用于 orgId 的用户id, 如果uid存在,则表示是用户是组织,其中uid为真实用户 | ||||||
|    * @param uid |    * @param uid | ||||||
|    * @returns |    * @returns | ||||||
|    */ |    */ | ||||||
|   async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year', expand: any = {}) { |   async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week', expand: any = {}) { | ||||||
|     const { id, username, type } = this; |     const { id, username, type } = this; | ||||||
|     const oauthUser: OauthUser = { |     const oauthUser: OauthUser = { | ||||||
|       id, |       id, | ||||||
| @@ -68,7 +69,7 @@ export class User extends Model { | |||||||
|    * @returns |    * @returns | ||||||
|    */ |    */ | ||||||
|   static async verifyToken(token: string) { |   static async verifyToken(token: string) { | ||||||
|     return await oauth.verifyToken(token); |     return await UserSecret.verifyToken(token); | ||||||
|   } |   } | ||||||
|   /** |   /** | ||||||
|    * 刷新token |    * 刷新token | ||||||
| @@ -80,15 +81,30 @@ export class User extends Model { | |||||||
|     return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken }; |     return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken }; | ||||||
|   } |   } | ||||||
|   static async getOauthUser(token: string) { |   static async getOauthUser(token: string) { | ||||||
|     return await oauth.verifyToken(token); |     return await UserSecret.verifyToken(token); | ||||||
|   } |   } | ||||||
|  |   /** | ||||||
|  |    * 清理用户的token,需要重新登陆 | ||||||
|  |    * @param userid | ||||||
|  |    * @param orgid | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   static async clearUserToken(userid: string, type: 'org' | 'user' = 'user') { | ||||||
|  |     return await oauth.expireUserTokens(userid, type); | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * 获取用户信息, 并设置tokenUser | ||||||
|  |    * @param token | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|   static async getUserByToken(token: string) { |   static async getUserByToken(token: string) { | ||||||
|     const oauthUser = await oauth.verifyToken(token); |     const oauthUser = await UserSecret.verifyToken(token); | ||||||
|     if (!oauthUser) { |     if (!oauthUser) { | ||||||
|       throw new CustomError('Token is invalid. get UserByToken'); |       throw new CustomError('Token is invalid. get UserByToken'); | ||||||
|     } |     } | ||||||
|     const userId = oauthUser?.uid || oauthUser.id; |     const userId = oauthUser?.uid || oauthUser.id; | ||||||
|     const user = await User.findByPk(userId); |     const user = await User.findByPk(userId); | ||||||
|  |     user.setTokenUser(oauthUser); | ||||||
|     return user; |     return user; | ||||||
|   } |   } | ||||||
|   /** |   /** | ||||||
| @@ -148,9 +164,15 @@ export class User extends Model { | |||||||
|     const cPassword = cryptPwd(password, salt); |     const cPassword = cryptPwd(password, salt); | ||||||
|     return this.password === cPassword; |     return this.password === cPassword; | ||||||
|   } |   } | ||||||
|   async getInfo() { |   /** | ||||||
|  |    * 获取用户信息, 需要先设置 tokenUser 或者设置 uid | ||||||
|  |    * @param uid 如果存在,则表示是组织,其中uid为真实用户 | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async getInfo(uid?: string) { | ||||||
|     const orgs = await this.getOrgs(); |     const orgs = await this.getOrgs(); | ||||||
|     return { |  | ||||||
|  |     const info: Record<string, any> = { | ||||||
|       id: this.id, |       id: this.id, | ||||||
|       username: this.username, |       username: this.username, | ||||||
|       nickname: this.nickname, |       nickname: this.nickname, | ||||||
| @@ -160,14 +182,25 @@ export class User extends Model { | |||||||
|       avatar: this.avatar, |       avatar: this.avatar, | ||||||
|       orgs, |       orgs, | ||||||
|     }; |     }; | ||||||
|  |     const tokenUser = this.tokenUser; | ||||||
|  |     if (uid) { | ||||||
|  |       info.uid = uid; | ||||||
|  |     } else if (tokenUser.uid) { | ||||||
|  |       info.uid = tokenUser.uid; | ||||||
|  |     } | ||||||
|  |     return info; | ||||||
|   } |   } | ||||||
|  |   /** | ||||||
|  |    * 获取用户组织 | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|   async getOrgs() { |   async getOrgs() { | ||||||
|     let id = this.id; |     let id = this.id; | ||||||
|     if (this.type === 'org') { |     if (this.type === 'org') { | ||||||
|       if (this.tokenUser && this.tokenUser.uid) { |       if (this.tokenUser && this.tokenUser.uid) { | ||||||
|         id = this.tokenUser.uid; |         id = this.tokenUser.uid; | ||||||
|       } else { |       } else { | ||||||
|         throw new CustomError('Permission denied'); |         throw new CustomError(400, 'Permission denied'); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     const cache = await redis.get(`user:${id}:orgs`); |     const cache = await redis.get(`user:${id}:orgs`); | ||||||
| @@ -198,7 +231,7 @@ export class User extends Model { | |||||||
| } | } | ||||||
| export type SyncOpts = { | export type SyncOpts = { | ||||||
|   alter?: boolean; |   alter?: boolean; | ||||||
|   logging?: boolean; |   logging?: any; | ||||||
|   force?: boolean; |   force?: boolean; | ||||||
| }; | }; | ||||||
| export const UserInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => { | export const UserInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => { | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { useContextKey, useContext } from '@kevisual/use-config/context'; |  | ||||||
| import { sequelize } from './sequelize.ts'; | import { sequelize } from './sequelize.ts'; | ||||||
|  |  | ||||||
| export { sequelize }; | export { sequelize }; | ||||||
|   | |||||||
| @@ -1,9 +1,15 @@ | |||||||
| import { Redis } from 'ioredis'; | import { Redis } from 'ioredis'; | ||||||
| import { useConfig } from '@kevisual/use-config'; | import { useConfig } from '@kevisual/use-config/env'; | ||||||
| import { useContextKey } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/context'; | ||||||
| const config = useConfig<{ | const configEnv = useConfig(); | ||||||
|   redis: ConstructorParameters<typeof Redis>; |  | ||||||
| }>(); | const redisConfig = { | ||||||
|  |   host: configEnv.REDIS_HOST || 'localhost', | ||||||
|  |   port: configEnv.REDIS_PORT ? parseInt(configEnv.REDIS_PORT) : 6379, | ||||||
|  |   password: configEnv.REDIS_PASSWORD || undefined, | ||||||
|  |   db: configEnv.REDIS_DB ? parseInt(configEnv.REDIS_DB) : 0, | ||||||
|  | }; | ||||||
|  |  | ||||||
| // 配置 Redis 连接 | // 配置 Redis 连接 | ||||||
| export const redis = new Redis({ | export const redis = new Redis({ | ||||||
|   host: 'localhost', // Redis 服务器的主机名或 IP 地址 |   host: 'localhost', // Redis 服务器的主机名或 IP 地址 | ||||||
| @@ -16,7 +22,6 @@ export const redis = new Redis({ | |||||||
|     return Math.min(times * 50, 2000); // 每次重试时延迟增加 |     return Math.min(times * 50, 2000); // 每次重试时延迟增加 | ||||||
|   }, |   }, | ||||||
|   maxRetriesPerRequest: null, // 允许请求重试的次数 (如果需要无限次重试) |   maxRetriesPerRequest: null, // 允许请求重试的次数 (如果需要无限次重试) | ||||||
|   ...config.redis, |  | ||||||
| }); | }); | ||||||
| useContextKey('redis', () => redis); | useContextKey('redis', () => redis); | ||||||
| // 监听连接事件 | // 监听连接事件 | ||||||
|   | |||||||
| @@ -1,19 +1,16 @@ | |||||||
| import { useConfig } from '@kevisual/use-config'; | import { useConfig } from '@kevisual/use-config/env'; | ||||||
| import { Sequelize } from 'sequelize'; | import { Sequelize } from 'sequelize'; | ||||||
| import { useContextKey, useContext } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/context'; | ||||||
|  |  | ||||||
| type PostgresConfig = { | const configEnv = useConfig(); | ||||||
|   postgres: { |  | ||||||
|     username: string; | const postgresConfig = { | ||||||
|     password: string; |   username: configEnv.POSTGRES_USERNAME, | ||||||
|     host: string; |   password: configEnv.POSTGRES_PASSWORD, | ||||||
|     port: number; |   host: configEnv.POSTGRES_HOST, | ||||||
|     database: string; |   port: configEnv.POSTGRES_PORT ? parseInt(configEnv.POSTGRES_PORT) : 5432, | ||||||
|   }; |   database: configEnv.POSTGRES_DATABASE, | ||||||
| }; | }; | ||||||
| const config = useConfig<PostgresConfig>(); |  | ||||||
|  |  | ||||||
| const postgresConfig = config.postgres; |  | ||||||
|  |  | ||||||
| if (!postgresConfig) { | if (!postgresConfig) { | ||||||
|   console.error('postgres config is required'); |   console.error('postgres config is required'); | ||||||
|   | |||||||
| @@ -2,5 +2,17 @@ import { OAuth, RedisTokenStore } from './oauth.ts'; | |||||||
| import { useContextKey } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/use-config/context'; | ||||||
| import { Redis } from 'ioredis'; | import { Redis } from 'ioredis'; | ||||||
|  |  | ||||||
| export const redis = useContextKey<Redis>('redis'); | export const oauth = useContextKey('oauth', () => { | ||||||
| export const oauth = useContextKey('oauth', () => new OAuth(new RedisTokenStore(redis))); |   const redis = useContextKey<Redis>('redis'); | ||||||
|  |   const store = new RedisTokenStore(redis); | ||||||
|  |   // redis是promise | ||||||
|  |   if (redis instanceof Promise) { | ||||||
|  |     redis.then((r) => { | ||||||
|  |       store.setRedis(r); | ||||||
|  |     }); | ||||||
|  |   } else if (redis) { | ||||||
|  |     store.setRedis(redis); | ||||||
|  |   } | ||||||
|  |   const oauth = new OAuth(store); | ||||||
|  |   return oauth; | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -58,26 +58,31 @@ export type UserExpand = { | |||||||
| } & StoreSetOpts; | } & StoreSetOpts; | ||||||
|  |  | ||||||
| type StoreSetOpts = { | type StoreSetOpts = { | ||||||
|   loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year' |   loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year' | ||||||
|   expire?: number; // 过期时间,单位为秒 |   expire?: number; // 过期时间,单位为秒 | ||||||
|   hasRefreshToken?: boolean; |   hasRefreshToken?: boolean; | ||||||
|   [key: string]: any; |   [key: string]: any; | ||||||
| }; | }; | ||||||
| interface Store<T> { | interface Store<T> { | ||||||
|  |   redis?: Redis; | ||||||
|   getObject: (key: string) => Promise<T>; |   getObject: (key: string) => Promise<T>; | ||||||
|   setObject: (key: string, value: T, opts?: StoreSetOpts) => Promise<void>; |   setObject: (key: string, value: T, opts?: StoreSetOpts) => Promise<void>; | ||||||
|   expire: (key: string, ttl?: number) => Promise<void>; |   expire: (key: string, ttl?: number) => Promise<void>; | ||||||
|   delObject: (value?: T) => Promise<void>; |   delObject: (value?: T) => Promise<void>; | ||||||
|   keys: (key?: string) => Promise<string[]>; |   keys: (key?: string) => Promise<string[]>; | ||||||
|   setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>; |   setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>; | ||||||
|  |   delKeys: (keys: string[]) => Promise<number>; | ||||||
| } | } | ||||||
| export class RedisTokenStore implements Store<OauthUser> { | export class RedisTokenStore implements Store<OauthUser> { | ||||||
|   private redis: Redis; |   redis: Redis; | ||||||
|   private prefix: string = 'oauth:'; |   private prefix: string = 'oauth:'; | ||||||
|   constructor(redis: Redis, prefix?: string) { |   constructor(redis?: Redis, prefix?: string) { | ||||||
|     this.redis = redis; |     this.redis = redis; | ||||||
|     this.prefix = prefix || this.prefix; |     this.prefix = prefix || this.prefix; | ||||||
|   } |   } | ||||||
|  |   async setRedis(redis: Redis) { | ||||||
|  |     this.redis = redis; | ||||||
|  |   } | ||||||
|   async set(key: string, value: string, ttl?: number) { |   async set(key: string, value: string, ttl?: number) { | ||||||
|     await this.redis.set(this.prefix + key, value, 'EX', ttl); |     await this.redis.set(this.prefix + key, value, 'EX', ttl); | ||||||
|   } |   } | ||||||
| @@ -137,6 +142,12 @@ export class RedisTokenStore implements Store<OauthUser> { | |||||||
|     let expire = opts?.expire; |     let expire = opts?.expire; | ||||||
|     if (!expire) { |     if (!expire) { | ||||||
|       switch (opts.loginType) { |       switch (opts.loginType) { | ||||||
|  |         case 'day': | ||||||
|  |           expire = 24 * 60 * 60; | ||||||
|  |           break; | ||||||
|  |         case 'week': | ||||||
|  |           expire = 7 * 24 * 60 * 60; | ||||||
|  |           break; | ||||||
|         case 'month': |         case 'month': | ||||||
|           expire = 30 * 24 * 60 * 60; |           expire = 30 * 24 * 60 * 60; | ||||||
|           break; |           break; | ||||||
| @@ -144,7 +155,7 @@ export class RedisTokenStore implements Store<OauthUser> { | |||||||
|           expire = 90 * 24 * 60 * 60; |           expire = 90 * 24 * 60 * 60; | ||||||
|           break; |           break; | ||||||
|         default: |         default: | ||||||
|           expire = 25 * 60 * 60; // 默认过期时间为25小时 |           expire = 7 * 24 * 60 * 60; // 默认过期时间为7天 | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       expire = Math.min(expire, 60 * 60 * 24 * 30, 60 * 60 * 24 * 90); // 默认的过期时间最大为90天 |       expire = Math.min(expire, 60 * 60 * 24 * 30, 60 * 60 * 24 * 90); // 默认的过期时间最大为90天 | ||||||
| @@ -162,6 +173,11 @@ export class RedisTokenStore implements Store<OauthUser> { | |||||||
|       await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire); |       await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |   async delKeys(keys: string[]) { | ||||||
|  |     const prefix = this.prefix; | ||||||
|  |     const number = await this.redis.del(keys.map((key) => prefix + key)); | ||||||
|  |     return number; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export class OAuth<T extends OauthUser> { | export class OAuth<T extends OauthUser> { | ||||||
| @@ -170,9 +186,21 @@ export class OAuth<T extends OauthUser> { | |||||||
|   constructor(store: Store<T>) { |   constructor(store: Store<T>) { | ||||||
|     this.store = store; |     this.store = store; | ||||||
|   } |   } | ||||||
|  |   generateSecretKey(sk = true) { | ||||||
|  |     if (sk) { | ||||||
|  |       return 'sk_' + randomId64(); | ||||||
|  |     } | ||||||
|  |     return 'st_' + randomId32(); | ||||||
|  |   } | ||||||
|   /** |   /** | ||||||
|    * 生成token |    * 生成token | ||||||
|    * @param user |    * @param user | ||||||
|  |    * @param user.id 访问者id | ||||||
|  |    * @param user.uid 如果是org,这个是真实用户id,id是orgId | ||||||
|  |    * @param user.userId 真实用户id | ||||||
|  |    * @param user.orgId 组织id,可选 | ||||||
|  |    * @param user.username | ||||||
|  |    * @param user.type | ||||||
|    * @returns |    * @returns | ||||||
|    */ |    */ | ||||||
|   async generateToken( |   async generateToken( | ||||||
| @@ -203,6 +231,37 @@ export class OAuth<T extends OauthUser> { | |||||||
|  |  | ||||||
|     return { accessToken, refreshToken }; |     return { accessToken, refreshToken }; | ||||||
|   } |   } | ||||||
|  |   async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) { | ||||||
|  |     // 生成一个secretKey | ||||||
|  |     // 设置到store中 | ||||||
|  |     oauthUser.oauthExpand = { | ||||||
|  |       ...oauthUser.oauthExpand, | ||||||
|  |       accessToken: secretKey, | ||||||
|  |       description: 'secretKey', | ||||||
|  |       createTime: new Date().getTime(), // 创建时间 | ||||||
|  |     }; | ||||||
|  |     await this.store.setToken( | ||||||
|  |       { accessToken: secretKey, refreshToken: '', value: oauthUser }, | ||||||
|  |       { | ||||||
|  |         ...opts, | ||||||
|  |         hasRefreshToken: false, | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |     return secretKey; | ||||||
|  |   } | ||||||
|  |   getOauthUser({ id, uid, username, type }: Partial<T>): OauthUser { | ||||||
|  |     const oauthUser: OauthUser = { | ||||||
|  |       id, | ||||||
|  |       username, | ||||||
|  |       uid, | ||||||
|  |       userId: uid || id, // 必存在,真实用户id | ||||||
|  |       type: type as 'user' | 'org', | ||||||
|  |     }; | ||||||
|  |     if (uid) { | ||||||
|  |       oauthUser.orgId = id; | ||||||
|  |     } | ||||||
|  |     return oauthUser; | ||||||
|  |   } | ||||||
|   /** |   /** | ||||||
|    * 验证token,如果token不存在,返回null |    * 验证token,如果token不存在,返回null | ||||||
|    * @param token |    * @param token | ||||||
| @@ -212,6 +271,21 @@ export class OAuth<T extends OauthUser> { | |||||||
|     const res = await this.store.getObject(token); |     const res = await this.store.getObject(token); | ||||||
|     return res; |     return res; | ||||||
|   } |   } | ||||||
|  |   /** | ||||||
|  |    * 验证token是否是accessToken, sk 开头的为secretKey,没有refreshToken | ||||||
|  |    * @param token | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   isSecretKey(token: string) { | ||||||
|  |     if (!token) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     // 如果是sk_开头,则是secretKey | ||||||
|  |     if (token.startsWith('sk_')) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|   /** |   /** | ||||||
|    * 刷新token |    * 刷新token | ||||||
|    * @param refreshToken |    * @param refreshToken | ||||||
| @@ -279,4 +353,40 @@ export class OAuth<T extends OauthUser> { | |||||||
|     } |     } | ||||||
|     this.store.delObject(user); |     this.store.delObject(user); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 获取某一个用户的所有token | ||||||
|  |    * @param userId | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async getUserTokens(userId: string, orgId?: string) { | ||||||
|  |     const userPrefix = orgId ? `org:${orgId}:user:${userId}` : `user:${userId}`; | ||||||
|  |     const tokens = await this.store.keys(`${userPrefix}:token:*`); | ||||||
|  |     return tokens; | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * 过期某一个用户的所有token | ||||||
|  |    * @param userId | ||||||
|  |    * @param orgId | ||||||
|  |    */ | ||||||
|  |   async expireUserTokens(userId: string, type: 'user' | 'org' = 'user') { | ||||||
|  |     const userPrefix = type === 'org' ? `org:${userId}:user:*:` : `user:${userId}`; | ||||||
|  |     const tokensKeys = await this.store.keys(`${userPrefix}:token:*`); | ||||||
|  |     for (const tokenKey of tokensKeys) { | ||||||
|  |       try { | ||||||
|  |         const token = await this.store.redis.get(tokenKey); | ||||||
|  |         const user = await this.store.getObject(token); | ||||||
|  |         this.store.delObject(user); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('expireUserTokens error', userId, type, error); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * 过期所有用户的token, 然后重启服务 | ||||||
|  |    */ | ||||||
|  |   async expireAllTokens() { | ||||||
|  |     const tokens = await this.store.keys('*'); | ||||||
|  |     await this.store.delKeys(tokens); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| /** | /** | ||||||
|  * 直接开发业务代码,把redis和sequelize的初始化放到库当中。 |  * 直接开发业务代码,把redis和sequelize的初始化放到库当中。 | ||||||
|  */ |  */ | ||||||
| import { useConfig } from '@kevisual/use-config'; |  | ||||||
| import { app } from './app.ts'; | import { app } from './app.ts'; | ||||||
| import * as sequelizeLib from './modules/sequelize.ts'; | import * as sequelizeLib from './modules/sequelize.ts'; | ||||||
| export const sequelize = useContextKey('sequelize', () => sequelizeLib.sequelize); | export const sequelize = useContextKey('sequelize', () => sequelizeLib.sequelize); | ||||||
| @@ -11,7 +10,6 @@ import { Org, OrgInit, OrgModel } from './models/org.ts'; | |||||||
|  |  | ||||||
| import * as redisLib from './modules/redis.ts'; | import * as redisLib from './modules/redis.ts'; | ||||||
| import { useContextKey } from '@kevisual/use-config/context'; | import { useContextKey } from '@kevisual/use-config/context'; | ||||||
| useConfig(); |  | ||||||
|  |  | ||||||
| export const redis = useContextKey('redis', () => redisLib.redis); | export const redis = useContextKey('redis', () => redisLib.redis); | ||||||
| export const redisPublisher = useContextKey('redisPublisher', () => redisLib.redisPublisher); | export const redisPublisher = useContextKey('redisPublisher', () => redisLib.redisPublisher); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user