mm_api 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/manager.js ADDED
@@ -0,0 +1,224 @@
1
+ const Manager = require("mm_corejs").Manager;
2
+ const Drive = require("./drive");
3
+
4
+ /**
5
+ * Api接口类(Web API 网关管理器,基于 mm_corejs,无全局/原型扩展依赖)
6
+ * @class
7
+ * @augments {Manager}
8
+ */
9
+ class Api extends Manager {
10
+ /**
11
+ * 配置参数
12
+ * @type {object}
13
+ */
14
+ static config = {
15
+ /**
16
+ * 名称
17
+ * @type {string}
18
+ */
19
+ name: "",
20
+ /**
21
+ * 标题
22
+ * @type {string}
23
+ */
24
+ title: "API接口",
25
+ /**
26
+ * 描述
27
+ * @type {string}
28
+ */
29
+ description: "这是API接口管理器",
30
+ /**
31
+ * 最大深度
32
+ * @type {number}
33
+ */
34
+ max_depth: 6,
35
+ /**
36
+ * 排序键(接口匹配排序用的配置字段名)
37
+ * @type {string}
38
+ */
39
+ sort_key: "sort",
40
+ /**
41
+ * 缓存实例获取函数(可选注入,返回 cache 实例;未配置则跳过接口缓存与缓存登录态)
42
+ * @type {Function|null}
43
+ */
44
+ get_cache: null,
45
+ /**
46
+ * 资源清单文件名(update 扫描目录时使用)
47
+ * @type {string}
48
+ */
49
+ filename: "api.json",
50
+ };
51
+
52
+ /**
53
+ * 构造函数
54
+ * @param {object} config 配置参数
55
+ * @param {object} parent 父级模块(仅作兼容传入,不使用)
56
+ */
57
+ constructor(config, parent) {
58
+ super({ ...Api.config, ...config }, parent);
59
+ // 插件注册表(方案B:手动注册,供驱动 plugin() 查找)
60
+ this.plugins = {};
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Api驱动类
66
+ * @type {Drive}
67
+ */
68
+ Api.prototype.Drive = Drive;
69
+
70
+ /**
71
+ * 注册一个 api 驱动(方案B:手动注册,不扫描目录)
72
+ * @param {object} config 驱动配置(必须包含 name)
73
+ * @returns {object} 驱动实例
74
+ */
75
+ Api.prototype.add = function (config) {
76
+ var drive = new Drive({ ...config }, this);
77
+ this.set(drive.config.name, drive);
78
+ return drive;
79
+ };
80
+
81
+ /**
82
+ * 注销一个 api 驱动(先停止再删除注册表项)
83
+ * @param {string} name 驱动名称
84
+ * @returns {Promise<boolean>} 是否删除成功
85
+ */
86
+ Api.prototype.del = async function (name) {
87
+ var drive = this.get(name);
88
+ if (drive) {
89
+ await drive.stop();
90
+ }
91
+ return await Manager.prototype.del.call(this, name);
92
+ };
93
+
94
+ /**
95
+ * 重启一个 api 驱动(stop 后重新 start)
96
+ * 注意:生命周期状态机不支持 stopped 后再次 start,失败时需 del 后重新 add 实现手动重启
97
+ * @param {string} name 驱动名称
98
+ * @returns {Promise<boolean>} 是否执行成功
99
+ */
100
+ Api.prototype.reload = async function (name) {
101
+ var drive = this.get(name);
102
+ if (!drive) {
103
+ this.log("warn", "驱动不存在,无法重启", name);
104
+ return false;
105
+ }
106
+ try {
107
+ await drive.stop();
108
+ await this._initDrive(drive);
109
+ await drive.start();
110
+ return true;
111
+ } catch (err) {
112
+ this.log("warn", "驱动重启失败(生命周期不支持停止后重启,请 del 后重新 add)", name, err);
113
+ return false;
114
+ }
115
+ };
116
+
117
+ /**
118
+ * 注册插件集合(供驱动 plugin() 查找,替代原全局插件池)
119
+ * @param {string} name 插件作用域名称(app 名)
120
+ * @param {object} obj 插件集合对象(需含 get 方法)
121
+ * @returns {object} 当前管理器实例
122
+ */
123
+ Api.prototype.usePlugin = function (name, obj) {
124
+ this.plugins[name] = obj;
125
+ return this;
126
+ };
127
+
128
+ /**
129
+ * 初始化驱动实例(仅未初始化时执行生命周期 init,重复调用安全)
130
+ * @param {object} drive 驱动实例
131
+ * @returns {Promise<void>} 初始化完成
132
+ * @private
133
+ */
134
+ Api.prototype._initDrive = async function (drive) {
135
+ if (drive.isState("created")) {
136
+ await drive.init();
137
+ }
138
+ };
139
+
140
+ /**
141
+ * 获取所有驱动配置列表(方案B:来自注册表,替代旧基类 getInfos)
142
+ * @returns {Array} 驱动配置数组
143
+ * @private
144
+ */
145
+ Api.prototype._getInfos = function () {
146
+ return this.values().map((d) => d.config);
147
+ };
148
+
149
+ /**
150
+ * 获取按匹配优先级排序后的启用驱动列表
151
+ * 排序规则:路径长的优先,其次按排序键升序(保留原 _sort 的比较语义)
152
+ * @returns {Array} 驱动实例数组
153
+ * @private
154
+ */
155
+ Api.prototype._sortedDrives = function () {
156
+ var sort_key = this.config.sort_key || "sort";
157
+ var drives = this.values().filter((d) => d.config.state === 1);
158
+ drives.sort((m1, m2) => {
159
+ var p1 = m1.config.path || "";
160
+ var p2 = m2.config.path || "";
161
+ var n = p2.length - p1.length;
162
+ if (n) {
163
+ return n;
164
+ } else {
165
+ return m1.config[sort_key] - m2.config[sort_key];
166
+ }
167
+ });
168
+ return drives;
169
+ };
170
+
171
+ /**
172
+ * 匹配目标
173
+ * @param {string} str 目标
174
+ * @param {string} pattern 模式
175
+ * @returns {boolean} 是否匹配
176
+ */
177
+ Api.prototype.isMatch = function (str, pattern) {
178
+ if (str === pattern) {
179
+ return true;
180
+ } else if (pattern.startsWith("*")) {
181
+ let clean = pattern.replaceAll("*", "");
182
+ if (pattern.endsWith("*")) {
183
+ return str.indexOf(clean) !== -1;
184
+ } else {
185
+ return str.endsWith(clean);
186
+ }
187
+ } else if (pattern.endsWith("*")) {
188
+ let clean = pattern.replaceAll("*", "");
189
+ return str.startsWith(clean);
190
+ } else {
191
+ return false;
192
+ }
193
+ };
194
+
195
+ /**
196
+ * 执行Api
197
+ * @param {object} ctx 请求上下文
198
+ * @param {object} db 数据管理器
199
+ * @returns {object | string} 返回一个对象或字符串
200
+ */
201
+ Api.prototype.run = async function (ctx, options = {}) {
202
+ let path = ctx.request.path;
203
+ let drives = this._sortedDrives();
204
+
205
+ for (var i = 0; i < drives.length; i++) {
206
+ var mod = drives[i];
207
+ if (this.isMatch(path, mod.config.path)) {
208
+ if (!mod.isLoaded()) {
209
+ await mod.load();
210
+ }
211
+ var ret = await mod.run(ctx, options);
212
+ if (ret) {
213
+ db.ret = ret;
214
+ break;
215
+ }
216
+ }
217
+ }
218
+ return db.ret;
219
+ };
220
+
221
+ /**
222
+ * @module 导出API管理器类
223
+ */
224
+ module.exports = Api;
package/lib/oauth.js ADDED
@@ -0,0 +1,277 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { string } = require("mm_helper");
4
+
5
+ /**
6
+ * 转换值类型(等价复刻 mm_expand 的 convertValue)
7
+ * @param {any} target_val 目标值
8
+ * @param {any} source_val 源值
9
+ * @returns {any} 转换后的值
10
+ */
11
+ function _convertValue(target_val, source_val) {
12
+ var target_type = typeof target_val;
13
+ if (target_type === "number") {
14
+ var num = Number(source_val);
15
+ return isNaN(num) ? 0 : num;
16
+ } else if (target_type === "boolean") {
17
+ if (source_val === "0" || source_val === "false" || source_val === "False") {
18
+ return false;
19
+ } else if (source_val === "1" || source_val === "true" || source_val === "True") {
20
+ return true;
21
+ } else {
22
+ return Boolean(source_val);
23
+ }
24
+ } else if (target_type === "string") {
25
+ return String(source_val);
26
+ } else {
27
+ return source_val;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 添加对象属性(替代原全局 push,等价复刻 mm_expand 的 push)
33
+ * @param {object} target 被添加的对象
34
+ * @param {object} source 用作添加的对象
35
+ * @param {boolean} supplement 是否补充没有的对象
36
+ * @returns {object} 修改后的目标对象
37
+ */
38
+ function _push(target, source, supplement = false) {
39
+ if (!target || !source || typeof target !== "object" || typeof source !== "object") {
40
+ return target;
41
+ }
42
+ for (var key in source) {
43
+ if (target.hasOwnProperty(key)) {
44
+ var target_val = target[key];
45
+ var source_val = source[key];
46
+ if (typeof target_val === "object" && typeof source_val === "object") {
47
+ _push(target_val, source_val, supplement);
48
+ } else if (typeof target_val === typeof source_val) {
49
+ target[key] = _convertValue(target_val, source_val);
50
+ }
51
+ } else if (supplement) {
52
+ target[key] = source[key];
53
+ }
54
+ }
55
+ return target;
56
+ }
57
+
58
+ /**
59
+ * Oauth身份验证驱动类
60
+ * @class
61
+ */
62
+ class Oauth {
63
+ static config = {
64
+ // 是否登录
65
+ 'sign_in': false,
66
+ // 要求会员级别
67
+ 'vip': 0,
68
+ // 要求管理级别
69
+ 'gm': 0,
70
+ // 要求商户级别
71
+ 'mc': 0,
72
+ // 要求用户组
73
+ 'user_group': [],
74
+ // 要求管理组
75
+ 'user_admin': [],
76
+ // 文件路径, 当调用函数不存在时,会先从文件中加载
77
+ 'main': '',
78
+ // 当前目录(方案B:由驱动传入,替代旧基类 getDir)
79
+ 'dir': '',
80
+ // 登录token请求头键名(替代原全局 dict 中的 token 键)
81
+ 'token_key': 'token',
82
+ // 会话缓存键前缀(替代原全局 dict 中的 session_id 键)
83
+ 'session_key': 'mm:uuid',
84
+ // 缓存实例获取函数(可选注入,未配置则跳过缓存登录态)
85
+ 'get_cache': null
86
+ };
87
+ /**
88
+ * 构造函数
89
+ * @param {object} config 配置参数(含 dir/token_key/session_key/get_cache)
90
+ * @class
91
+ */
92
+ constructor(config) {
93
+ // 配置参数
94
+ this.config = { ...Oauth.config, ...config };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 获取当前目录(方案B:来自构造注入的 dir)
100
+ * @returns {string} 当前目录
101
+ */
102
+ Oauth.prototype.getDir = function () {
103
+ return this.config.dir || __dirname;
104
+ };
105
+
106
+ /**
107
+ * 加载配置对象
108
+ * @param {object} obj 配置对象
109
+ */
110
+ Oauth.prototype.loadObj = function (obj) {
111
+ _push(this.config, obj);
112
+ var f = this.config.main;
113
+ if (f) {
114
+ var file = path.isAbsolute(f) ? f : path.resolve(this.getDir(), f);
115
+ if (fs.existsSync(file)) {
116
+ var cs = require(file);
117
+ if (cs) {
118
+ _push(this, cs, true);
119
+ }
120
+ }
121
+ }
122
+ };
123
+
124
+ /**
125
+ * 身份验证主程序
126
+ * @param {object} ctx HTTP请求上下文
127
+ * @returns {object} 验证失败返回错误提示,验证通过返回null
128
+ */
129
+ Oauth.prototype.main = async function (ctx) {
130
+ return null;
131
+ };
132
+
133
+ /**
134
+ * 小校验
135
+ * @param {object} ctx HTTP请求上下文
136
+ */
137
+ Oauth.prototype.checkSub = async function (ctx) {
138
+
139
+ };
140
+
141
+ /**
142
+ * 验证身份
143
+ * @param {object} ctx HTTP请求上下文
144
+ * @returns {object} 验证失败返回错误提示,验证通过返回null
145
+ */
146
+ Oauth.prototype.check = async function (ctx) {
147
+ // 无需登录则不验证身份和权限
148
+ var cg = this.config;
149
+ var user = await this._getUser(ctx);
150
+ if (!cg.sign_in) {
151
+ return null;
152
+ }
153
+ var error = await this.main(ctx);
154
+ if (error) {
155
+ return error;
156
+ }
157
+ if (!user) {
158
+ return {
159
+ code: 60000,
160
+ message: '账户未登录!'
161
+ };
162
+ }
163
+ if (await this._checkAccess(user, cg)) {
164
+ return {
165
+ code: 70000,
166
+ message: '没有访问权限'
167
+ };
168
+ }
169
+ return null;
170
+ };
171
+
172
+ /**
173
+ * 获取用户信息
174
+ * @param {object} ctx HTTP请求上下文
175
+ * @returns {object|null} 用户信息
176
+ * @private
177
+ */
178
+ Oauth.prototype._getUser = async function (ctx) {
179
+ var user = ctx.session?.user;
180
+ if (user) {
181
+ return user;
182
+ }
183
+ var token = ctx.headers[this.config.token_key];
184
+ if (!token) {
185
+ return null;
186
+ }
187
+ var cache = typeof this.config.get_cache === 'function' ? this.config.get_cache() : null;
188
+ if (!cache) {
189
+ return null;
190
+ }
191
+ var u = await cache.get(this.config.session_key + '_' + token);
192
+ if (!u) {
193
+ return null;
194
+ }
195
+ var user_info = this._extractUserFromCache(u);
196
+ if (user_info) {
197
+ ctx.session.user = user_info;
198
+ }
199
+ return user_info;
200
+ };
201
+
202
+ /**
203
+ * 从缓存中提取用户信息
204
+ * @param {object|string} u 缓存数据
205
+ * @returns {object|null} 用户信息
206
+ * @private
207
+ */
208
+ Oauth.prototype._extractUserFromCache = function (u) {
209
+ var type = typeof (u);
210
+ if (type === 'string') {
211
+ var o = string.toJson(u);
212
+ return o.user;
213
+ } else if (type === 'object' && Object.keys(u).length > 0) {
214
+ return u.user;
215
+ }
216
+ return null;
217
+ };
218
+
219
+ /**
220
+ * 检查用户访问权限
221
+ * @param {object} user 用户信息
222
+ * @param {object} config 配置信息
223
+ * @returns {boolean} 是否无权限
224
+ * @private
225
+ */
226
+ Oauth.prototype._checkAccess = function (user, config) {
227
+ return this._checkAccessLevels(user, config) ||
228
+ this._checkUserGroups(user, config) ||
229
+ this._checkAdminGroups(user, config);
230
+ };
231
+
232
+ /**
233
+ * 检查用户级别权限
234
+ * @param {object} user 用户信息
235
+ * @param {object} config 配置信息
236
+ * @returns {boolean} 是否无权限
237
+ * @private
238
+ */
239
+ Oauth.prototype._checkAccessLevels = function (user, config) {
240
+ return (config.gm !== 0 && user.gm < config.gm) ||
241
+ (config.vip !== 0 && user.vip < config.vip) ||
242
+ (config.mc !== 0 && user.mc < config.mc);
243
+ };
244
+
245
+ /**
246
+ * 检查用户组权限
247
+ * @param {object} user 用户信息
248
+ * @param {object} config 配置信息
249
+ * @returns {boolean} 是否无权限
250
+ * @private
251
+ */
252
+ Oauth.prototype._checkUserGroups = function (user, config) {
253
+ if (config.user_group.length === 0) {
254
+ return false;
255
+ }
256
+ var user_groups = user.user_group.split(',');
257
+ var has_group = config.user_group.some(group => user_groups.includes(group));
258
+ return !has_group;
259
+ };
260
+
261
+ /**
262
+ * 检查管理组权限
263
+ * @param {object} user 用户信息
264
+ * @param {object} config 配置信息
265
+ * @returns {boolean} 是否无权限
266
+ * @private
267
+ */
268
+ Oauth.prototype._checkAdminGroups = function (user, config) {
269
+ if (config.user_admin.length === 0) {
270
+ return false;
271
+ }
272
+ var admin_groups = user.user_admin.split(',');
273
+ var has_admin = config.user_admin.some(group => admin_groups.includes(group));
274
+ return !has_admin;
275
+ };
276
+
277
+ module.exports = Oauth;
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "mm_api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "API框架,用于实现web api和mvc模式路由请求,负责配置路由路径、访问权限、校验机制;基于 mm_corejs,内置管理器池,无全局/原型扩展依赖",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "mm_mod",
8
+ "mm_port",
9
+ "mm_in_port",
10
+ "api",
11
+ "mvc",
12
+ "router",
13
+ "oauth",
14
+ "API管理器"
15
+ ],
16
+ "layer": "port",
17
+ "group": "web",
18
+ "author": "邱文武",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "mm_corejs": "^1.1.4",
22
+ "mm_param": "^1.1.0",
23
+ "mm_helper": "^1.0.0",
24
+ "mm_sql": "^1.8.0"
25
+ },
26
+ "scripts": {
27
+ "test": "node test.js"
28
+ }
29
+ }
package/script.tpl.js ADDED
@@ -0,0 +1,32 @@
1
+
2
+ module.exports = {
3
+ /**
4
+ * 接口主函数
5
+ * @param {object} ctx HTTP上下文
6
+ * @param {object} db 数据管理器,如: { next: async function{}, ret: {} }
7
+ * @returns {object} 执行结果
8
+ */
9
+ async main(ctx, options) {
10
+ // 获取请求参数
11
+ var req = ctx.request;
12
+ var {
13
+ query,
14
+ body
15
+ } = req;
16
+ // return "hello world" + JSON.stringify(body);
17
+
18
+ // 操作sql模板
19
+ var m = query.method;
20
+ if (m === 'add') {
21
+
22
+ } else if (m === 'del') {
23
+
24
+ } else if (m === 'set') {
25
+
26
+ } else {
27
+
28
+ }
29
+ var ret = await this.sql.run(query, body, db);
30
+ return ret;
31
+ }
32
+ };
package/test.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ const assert = require('assert');
3
+ const mod = require('./index.js');
4
+
5
+ // 验证导出结构
6
+ assert.ok(mod.Manager, '应导出 Manager');
7
+ assert.ok(mod.admin, '应导出 admin');
8
+ assert.strictEqual(typeof mod.apiAdmin, 'function', '应导出 apiAdmin 函数');
9
+
10
+ // 验证通过工厂函数获取实例
11
+ const inst = mod.apiAdmin('test');
12
+ assert.ok(inst instanceof mod.Manager, 'apiAdmin 返回的实例应为 Manager 类型');
13
+
14
+ // 验证通过 admin 获取实例
15
+ const inst2 = mod.admin.get('test2');
16
+ assert.ok(inst2 instanceof mod.Manager, 'admin.get 返回的实例应为 Manager 类型');
17
+
18
+ console.log('mm_api 模块测试通过');