@kne/fastify-signature 1.0.2 → 1.0.4

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.
@@ -1,4 +1,8 @@
1
1
  const fp = require('fastify-plugin');
2
+ const { str, bool, any, success, empty, pageResult, STATUS, secret } = require('../schema/response');
3
+
4
+ const ADMIN_TAG = '签名管理后台-密钥';
5
+ const TAG = '签名-验签';
2
6
 
3
7
  module.exports = fp(async (fastify, options) => {
4
8
  const { services } = fastify[options.name];
@@ -9,7 +13,8 @@ module.exports = fp(async (fastify, options) => {
9
13
  {
10
14
  onRequest: [userAuth, adminAuth],
11
15
  schema: {
12
- description: '获取密钥列表',
16
+ tags: [ADMIN_TAG],
17
+ description: '分页查询全部用户的密钥,按创建时间倒序;密钥只返回脱敏值',
13
18
  summary: '获取密钥列表',
14
19
  query: {
15
20
  type: 'object',
@@ -23,9 +28,20 @@ module.exports = fp(async (fastify, options) => {
23
28
  type: 'number',
24
29
  description: '每页数量',
25
30
  default: 20
31
+ },
32
+ filter: {
33
+ type: 'object',
34
+ additionalProperties: true,
35
+ description: '筛选条件',
36
+ properties: {
37
+ keyword: { type: 'string', description: '关键字(AppId/描述/所属用户)' },
38
+ appId: { type: 'string', description: 'AppId' },
39
+ status: { description: '状态: 0开启,1禁用' }
40
+ }
26
41
  }
27
42
  }
28
- }
43
+ },
44
+ response: success('密钥分页列表', pageResult('密钥', secret))
29
45
  }
30
46
  },
31
47
  async request => {
@@ -38,7 +54,8 @@ module.exports = fp(async (fastify, options) => {
38
54
  {
39
55
  onRequest: [userAuth, adminAuth],
40
56
  schema: {
41
- description: '创建密钥',
57
+ tags: [ADMIN_TAG],
58
+ description: '生成一对 AppId / 密钥,密钥明文只在创建时返回一次',
42
59
  summary: '创建密钥',
43
60
  body: {
44
61
  type: 'object',
@@ -46,9 +63,19 @@ module.exports = fp(async (fastify, options) => {
46
63
  description: {
47
64
  type: 'string',
48
65
  description: '描述'
66
+ },
67
+ userId: {
68
+ description: '密钥所属用户 ID,不传为当前登录用户;验签通过后以该用户身份访问开放接口'
49
69
  }
50
70
  }
51
- }
71
+ },
72
+ response: success('创建的密钥', {
73
+ type: 'object',
74
+ properties: {
75
+ appId: str('AppId'),
76
+ secretKey: str('密钥明文,只返回这一次')
77
+ }
78
+ })
52
79
  }
53
80
  },
54
81
  async request => {
@@ -61,7 +88,8 @@ module.exports = fp(async (fastify, options) => {
61
88
  {
62
89
  onRequest: [userAuth, adminAuth],
63
90
  schema: {
64
- description: '删除密钥',
91
+ tags: [ADMIN_TAG],
92
+ description: '删除密钥,删除后该 AppId 无法再通过验签;AppId 不存在时报错',
65
93
  summary: '删除密钥',
66
94
  body: {
67
95
  type: 'object',
@@ -71,7 +99,8 @@ module.exports = fp(async (fastify, options) => {
71
99
  description: '密钥ID'
72
100
  }
73
101
  }
74
- }
102
+ },
103
+ response: empty('删除成功')
75
104
  }
76
105
  },
77
106
  async request => {
@@ -85,7 +114,8 @@ module.exports = fp(async (fastify, options) => {
85
114
  {
86
115
  onRequest: [userAuth, adminAuth],
87
116
  schema: {
88
- description: '更新密钥',
117
+ tags: [ADMIN_TAG],
118
+ description: '修改密钥状态与描述,未传的字段保持原值;AppId 不存在时报错',
89
119
  summary: '更新密钥',
90
120
  body: {
91
121
  type: 'object',
@@ -96,14 +126,15 @@ module.exports = fp(async (fastify, options) => {
96
126
  },
97
127
  status: {
98
128
  type: 'number',
99
- description: '密钥状态'
129
+ description: `密钥${STATUS}`
100
130
  },
101
131
  description: {
102
132
  type: 'string',
103
133
  description: '描述'
104
134
  }
105
135
  }
106
- }
136
+ },
137
+ response: empty('更新成功')
107
138
  }
108
139
  },
109
140
  async request => {
@@ -116,7 +147,8 @@ module.exports = fp(async (fastify, options) => {
116
147
  `${options.prefix}/verify`,
117
148
  {
118
149
  schema: {
119
- description: '验证密钥',
150
+ tags: [TAG],
151
+ description: '免登录。按 HMAC-SHA256(`${appId}|${timestamp}|${expire}`, 密钥) 校验签名,通过后记录最后访问时间;签名错误或过期返回 result=false,AppId 不存在时报错',
120
152
  summary: '验证密钥',
121
153
  body: {
122
154
  type: 'object',
@@ -127,18 +159,26 @@ module.exports = fp(async (fastify, options) => {
127
159
  },
128
160
  timestamp: {
129
161
  type: 'number',
130
- description: '时间戳'
162
+ description: '签名时的时间戳(秒)'
131
163
  },
132
164
  signature: {
133
165
  type: 'string',
134
- description: '签名'
166
+ description: '签名(hex)'
135
167
  },
136
168
  expire: {
137
169
  type: 'number',
138
- description: '过期时间'
170
+ description: '过期时间戳(秒),当前时间超过即验签失败'
139
171
  }
140
172
  }
141
- }
173
+ },
174
+ response: success('验签结果', {
175
+ type: 'object',
176
+ properties: {
177
+ result: bool('是否通过验签'),
178
+ message: str('结果说明'),
179
+ userId: any('密钥所属用户 ID,仅通过验签时返回')
180
+ }
181
+ })
142
182
  }
143
183
  },
144
184
  async request => {
@@ -0,0 +1,39 @@
1
+ // 响应 schema 会参与 fast-json-stringify 序列化:对象一律 additionalProperties: true 以免裁掉未声明字段;
2
+ // 用 nullable 而非类型数组(类型数组遇到不匹配的值会直接 500),类型不确定的字段(如外键 id、日期)只写说明不声明类型
3
+ const str = description => ({ type: 'string', nullable: true, description });
4
+ const num = description => ({ type: 'number', nullable: true, description });
5
+ const bool = description => ({ type: 'boolean', nullable: true, description });
6
+ const any = description => ({ description });
7
+ const list = (description, items) => ({ type: 'array', nullable: true, description, items });
8
+ const obj = (description, properties = {}) => ({ type: 'object', nullable: true, description, additionalProperties: true, properties });
9
+
10
+ const success = (description, schema) => ({
11
+ 200: Object.assign({}, schema || { type: 'object' }, { description })
12
+ });
13
+
14
+ const empty = (description = '操作成功,返回空对象') => success(description);
15
+
16
+ const pageResult = (description, item) => ({
17
+ type: 'object',
18
+ additionalProperties: true,
19
+ properties: {
20
+ pageData: list(`当前页${description}列表`, item),
21
+ totalCount: num('符合条件的总条数')
22
+ }
23
+ });
24
+
25
+ const STATUS = '状态:0 开启,1 禁用';
26
+
27
+ const secret = obj('密钥', {
28
+ id: str('密钥 ID(同 appId)'),
29
+ appId: str('AppId,即密钥 ID'),
30
+ secretKey: str('脱敏后的密钥,只保留首尾各 3 位'),
31
+ user: any('所属用户,未关联到用户时为 null'),
32
+ description: str('描述'),
33
+ lastVisitedAt: any('最后一次验签成功的时间'),
34
+ status: num(STATUS),
35
+ createdAt: any('创建时间'),
36
+ updatedAt: any('更新时间')
37
+ });
38
+
39
+ module.exports = { str, num, bool, any, list, obj, success, empty, pageResult, STATUS, secret };
@@ -3,16 +3,27 @@ const crypto = require('crypto');
3
3
 
4
4
  module.exports = fp(async (fastify, options) => {
5
5
  const { models, services } = fastify[options.name];
6
+ const { Op } = fastify.sequelize.Sequelize;
7
+
8
+ const escapeLike = value => String(value).replace(/[\\%_]/g, '\\$&');
9
+
10
+ const resolveAppId = appId => {
11
+ if (appId == null || appId === '') {
12
+ throw new Error('Invalid appId');
13
+ }
14
+ // 雪花 id 超过 Number.MAX_SAFE_INTEGER,必须始终按字符串查,禁止 Number(appId)
15
+ return String(appId);
16
+ };
6
17
 
7
18
  const create = async (authenticatePayload, { description, userId: targetUserId }) => {
8
19
  const { id: userId } = authenticatePayload;
9
20
  const secretKey = crypto.randomBytes(options.secretLength).toString('hex');
10
21
  const newSecret = await models.secret.create({ secretKey, description, userId: targetUserId || userId });
11
- return { appId: newSecret.id, secretKey: newSecret.secretKey };
22
+ return { appId: String(newSecret.id), secretKey: newSecret.secretKey };
12
23
  };
13
24
 
14
25
  const verify = async ({ appId, timestamp, expire, signature }) => {
15
- const secret = await models.secret.findByPk(appId);
26
+ const secret = await models.secret.findByPk(resolveAppId(appId));
16
27
  if (!secret) {
17
28
  throw new Error('Invalid appId');
18
29
  }
@@ -47,19 +58,49 @@ module.exports = fp(async (fastify, options) => {
47
58
  };
48
59
  };
49
60
 
50
- const list = async (authenticatePayload, { perPage, currentPage }) => {
51
- const { id: userId } = authenticatePayload;
61
+ const list = async (authenticatePayload, { perPage = 20, currentPage = 1, filter }) => {
62
+ filter = Object.assign({}, filter);
63
+ const whereQuery = {};
64
+ const sequelize = fastify.sequelize;
65
+ const castIdLike = value =>
66
+ sequelize.where(sequelize.cast(sequelize.col('secret.id'), 'TEXT'), {
67
+ [Op.iLike]: `%${escapeLike(value)}%`
68
+ });
69
+
70
+ const keyword = filter.keyword != null ? String(filter.keyword).trim() : '';
71
+ if (keyword) {
72
+ const like = { [Op.iLike]: `%${escapeLike(keyword)}%` };
73
+ whereQuery[Op.or] = [castIdLike(keyword), { description: like }, { '$user.nickname$': like }, { '$user.email$': like }, { '$user.phone$': like }];
74
+ }
75
+
76
+ const appId = filter.appId != null ? String(filter.appId).trim() : '';
77
+ if (appId) {
78
+ whereQuery[Op.and] = [...(whereQuery[Op.and] || []), castIdLike(appId)];
79
+ }
80
+
81
+ if (filter.status !== undefined && filter.status !== null && filter.status !== '') {
82
+ whereQuery.status = Number(filter.status);
83
+ }
84
+
52
85
  const { count, rows } = await models.secret.findAndCountAll({
53
- include: options.getUserModel(),
54
- where: {},
86
+ include: [
87
+ {
88
+ model: options.getUserModel(),
89
+ required: false
90
+ }
91
+ ],
92
+ where: whereQuery,
55
93
  limit: perPage,
56
- offset: (currentPage - 1) * perPage
94
+ offset: (currentPage - 1) * perPage,
95
+ order: [['createdAt', 'DESC']],
96
+ distinct: true
57
97
  });
58
98
  return {
59
99
  pageData: rows.map(item => {
100
+ const id = String(item.id);
60
101
  return {
61
- id: item.id,
62
- appId: item.id,
102
+ id,
103
+ appId: id,
63
104
  secretKey: item.secretKey.substring(0, 3) + '*'.repeat(8) + item.secretKey.substring(item.secretKey.length - 3),
64
105
  user: item.user,
65
106
  description: item.description,
@@ -73,27 +114,20 @@ module.exports = fp(async (fastify, options) => {
73
114
  };
74
115
  };
75
116
 
117
+ // list/update/remove 均为 admin 接口,可管理任意用户密钥;勿再按创建者校验归属
76
118
  const remove = async (authenticatePayload, { appId }) => {
77
- const { id: userId } = authenticatePayload;
78
- const secret = await models.secret.findByPk(appId);
119
+ const secret = await models.secret.findByPk(resolveAppId(appId));
79
120
  if (!secret) {
80
121
  throw new Error('Invalid appId');
81
122
  }
82
- if (secret.userId !== userId) {
83
- throw new Error('Invalid appId');
84
- }
85
123
  await secret.destroy();
86
124
  };
87
125
 
88
126
  const update = async (authenticatePayload, { appId, status, description }) => {
89
- const { id: userId } = authenticatePayload;
90
- const secret = await models.secret.findByPk(appId);
127
+ const secret = await models.secret.findByPk(resolveAppId(appId));
91
128
  if (!secret) {
92
129
  throw new Error('Invalid appId');
93
130
  }
94
- if (secret.userId !== userId) {
95
- throw new Error('Invalid appId');
96
- }
97
131
  await secret.update({ status, description });
98
132
  };
99
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kne/fastify-signature",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "用于实现一个签名和验证算法",
5
5
  "main": "index.js",
6
6
  "scripts": {