Compare commits
8 Commits
0c36328ac3
...
main
Author | SHA1 | Date | |
---|---|---|---|
f694299059 | |||
922b0c421f | |||
83f65e1554 | |||
2547355964 | |||
ad0d2e717f | |||
0a72db7771 | |||
162d4c72b4 | |||
60c5a986ed |
50
package.json
50
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/code-center-module",
|
"name": "@kevisual/code-center-module",
|
||||||
"version": "0.0.16",
|
"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.7",
|
"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.14",
|
"@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.38.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/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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();
|
||||||
};
|
};
|
||||||
|
321
src/mark/mark-model.ts
Normal file
321
src/mark/mark-model.ts
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
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 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,
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
};
|
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,7 +81,16 @@ 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
|
* 获取用户信息, 并设置tokenUser
|
||||||
@@ -88,7 +98,7 @@ export class User extends Model {
|
|||||||
* @returns
|
* @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');
|
||||||
}
|
}
|
||||||
@@ -221,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