@waline/vercel 1.15.3 → 1.17.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/vercel",
3
- "version": "1.15.3",
3
+ "version": "1.17.1",
4
4
  "description": "vercel server for waline comment system",
5
5
  "keywords": [
6
6
  "waline",
@@ -20,20 +20,21 @@
20
20
  "akismet": "^2.0.7",
21
21
  "deta": "^1.1.0",
22
22
  "dompurify": "^2.3.6",
23
+ "dy-node-ip2region": "^1.0.1",
23
24
  "fast-csv": "^4.3.6",
24
25
  "jsdom": "^19.0.0",
25
26
  "jsonwebtoken": "^8.5.1",
26
27
  "katex": "^0.15.3",
27
28
  "leancloud-storage": "^4.12.2",
28
- "markdown-it": "^12.3.2",
29
- "markdown-it-emoji": "^2.0.0",
29
+ "markdown-it": "^13.0.1",
30
+ "markdown-it-emoji": "^2.0.2",
30
31
  "markdown-it-sub": "^1.0.0",
31
32
  "markdown-it-sup": "^1.0.0",
32
33
  "mathjax-full": "^3.2.0",
33
- "nodemailer": "^6.7.3",
34
+ "nodemailer": "^6.7.4",
34
35
  "nunjucks": "^3.2.3",
35
36
  "phpass": "^0.1.1",
36
- "prismjs": "^1.27.0",
37
+ "prismjs": "^1.28.0",
37
38
  "request": "^2.88.2",
38
39
  "request-promise-native": "^1.0.9",
39
40
  "speakeasy": "^2.0.0",
@@ -14,6 +14,7 @@ const {
14
14
  TCB_KEY,
15
15
  SECURE_DOMAINS,
16
16
  DISABLE_USERAGENT,
17
+ DISABLE_REGION,
17
18
  AVATAR_PROXY,
18
19
  GITHUB_TOKEN,
19
20
  DETA_PROJECT_KEY,
@@ -106,6 +107,7 @@ module.exports = {
106
107
  disallowIPList: [],
107
108
  secureDomains: SECURE_DOMAINS ? SECURE_DOMAINS.split(/\s*,\s*/) : undefined,
108
109
  disableUserAgent: DISABLE_USERAGENT && !isFalse(DISABLE_USERAGENT),
110
+ disableRegion: DISABLE_REGION && !isFalse(DISABLE_REGION),
109
111
  levels:
110
112
  !LEVELS || isFalse(LEVELS)
111
113
  ? false
@@ -5,7 +5,7 @@ const { getMarkdownParser } = require('../service/markdown');
5
5
 
6
6
  const markdownParser = getMarkdownParser();
7
7
  async function formatCmt(
8
- { ua, user_id, ...comment },
8
+ { ua, user_id, ip, ...comment },
9
9
  users = [],
10
10
  { avatarProxy },
11
11
  loginUser
@@ -25,6 +25,7 @@ async function formatCmt(
25
25
  comment.mail = user.email;
26
26
  comment.link = user.url;
27
27
  comment.type = user.type;
28
+ comment.label = user.label;
28
29
  }
29
30
 
30
31
  const avatarUrl =
@@ -41,8 +42,13 @@ async function formatCmt(
41
42
  delete comment.mail;
42
43
  } else {
43
44
  comment.orig = comment.comment;
45
+ comment.ip = ip;
44
46
  }
45
47
 
48
+ // administrator can always show region
49
+ if (isAdmin || !think.config('disableRegion')) {
50
+ comment.addr = await think.ip2region(ip, { depth: isAdmin ? 3 : 1 });
51
+ }
46
52
  comment.comment = markdownParser(comment.comment);
47
53
  return comment;
48
54
  }
@@ -88,6 +94,7 @@ module.exports = class extends BaseRest {
88
94
  'pid',
89
95
  'rid',
90
96
  'ua',
97
+ 'ip',
91
98
  'user_id',
92
99
  'sticky',
93
100
  ],
@@ -106,7 +113,14 @@ module.exports = class extends BaseRest {
106
113
  users = await userModel.select(
107
114
  { objectId: ['IN', user_ids] },
108
115
  {
109
- field: ['display_name', 'email', 'url', 'type', 'avatar'],
116
+ field: [
117
+ 'display_name',
118
+ 'email',
119
+ 'url',
120
+ 'type',
121
+ 'avatar',
122
+ 'label',
123
+ ],
110
124
  }
111
125
  );
112
126
  }
@@ -186,7 +200,14 @@ module.exports = class extends BaseRest {
186
200
  users = await userModel.select(
187
201
  { objectId: ['IN', user_ids] },
188
202
  {
189
- field: ['display_name', 'email', 'url', 'type', 'avatar'],
203
+ field: [
204
+ 'display_name',
205
+ 'email',
206
+ 'url',
207
+ 'type',
208
+ 'avatar',
209
+ 'label',
210
+ ],
190
211
  }
191
212
  );
192
213
  }
@@ -235,6 +256,7 @@ module.exports = class extends BaseRest {
235
256
  'pid',
236
257
  'rid',
237
258
  'ua',
259
+ 'ip',
238
260
  'user_id',
239
261
  'sticky',
240
262
  ],
@@ -307,7 +329,14 @@ module.exports = class extends BaseRest {
307
329
  users = await userModel.select(
308
330
  { objectId: ['IN', user_ids] },
309
331
  {
310
- field: ['display_name', 'email', 'url', 'type', 'avatar'],
332
+ field: [
333
+ 'display_name',
334
+ 'email',
335
+ 'url',
336
+ 'type',
337
+ 'avatar',
338
+ 'label',
339
+ ],
311
340
  }
312
341
  );
313
342
  }
@@ -11,6 +11,26 @@ module.exports = class extends BaseRest {
11
11
  );
12
12
  }
13
13
 
14
+ async getAction() {
15
+ const { page, pageSize } = this.get();
16
+
17
+ const count = await this.modelInstance.count({});
18
+ const users = await this.modelInstance.select(
19
+ {},
20
+ {
21
+ desc: 'createdAt',
22
+ limit: pageSize,
23
+ offset: Math.max((page - 1) * pageSize, 0),
24
+ }
25
+ );
26
+ return this.success({
27
+ page,
28
+ totalPages: Math.ceil(count / pageSize),
29
+ pageSize,
30
+ data: users,
31
+ });
32
+ }
33
+
14
34
  async postAction() {
15
35
  const data = this.post();
16
36
  const resp = await this.modelInstance.select({
@@ -92,12 +112,20 @@ module.exports = class extends BaseRest {
92
112
  }
93
113
 
94
114
  async putAction() {
95
- const { display_name, url, avatar, password } = this.post();
115
+ const { display_name, url, avatar, password, type, label } = this.post();
96
116
  const { objectId } = this.ctx.state.userInfo;
97
117
  const twoFactorAuth = this.post('2fa');
98
118
 
99
119
  const updateData = {};
100
120
 
121
+ if (this.id && type) {
122
+ updateData.type = type;
123
+ }
124
+
125
+ if (think.isString(label)) {
126
+ updateData.label = label;
127
+ }
128
+
101
129
  if (display_name) {
102
130
  updateData.display_name = display_name;
103
131
  }
@@ -130,7 +158,9 @@ module.exports = class extends BaseRest {
130
158
  return this.success();
131
159
  }
132
160
 
133
- await this.modelInstance.update(updateData, { objectId });
161
+ await this.modelInstance.update(updateData, {
162
+ objectId: this.id || objectId,
163
+ });
134
164
 
135
165
  return this.success();
136
166
  }
@@ -1,5 +1,9 @@
1
+ const ip2region = require('dy-node-ip2region');
2
+ const helper = require('think-helper');
1
3
  const preventMessage = 'PREVENT_NEXT_PROCESS';
2
4
 
5
+ const regionSearch = ip2region.create();
6
+
3
7
  module.exports = {
4
8
  prevent() {
5
9
  throw new Error(preventMessage);
@@ -20,6 +24,10 @@ module.exports = {
20
24
  },
21
25
  promiseAllQueue(promises, taskNum) {
22
26
  return new Promise((resolve, reject) => {
27
+ if (!promises.length) {
28
+ return resolve();
29
+ }
30
+
23
31
  const ret = [];
24
32
  let index = 0;
25
33
  let count = 0;
@@ -46,4 +54,18 @@ module.exports = {
46
54
  }
47
55
  });
48
56
  },
57
+ async ip2region(ip, { depth = 1 }) {
58
+ if (!ip) return '';
59
+
60
+ try {
61
+ const search = helper.promisify(regionSearch.btreeSearch, regionSearch);
62
+ const { region } = await search(ip);
63
+ const [, , province, city, isp] = region.split('|');
64
+ const address = Array.from(new Set([province, city, isp]));
65
+ return address.slice(0, depth).join(' ');
66
+ } catch (e) {
67
+ console.log(e);
68
+ return '';
69
+ }
70
+ },
49
71
  };
package/src/logic/base.js CHANGED
@@ -8,6 +8,7 @@ module.exports = class extends think.Logic {
8
8
  `storage/${this.config('storage')}`,
9
9
  'Users'
10
10
  );
11
+ this.id = this.getId();
11
12
  }
12
13
 
13
14
  async __before() {
@@ -89,4 +90,20 @@ module.exports = class extends think.Logic {
89
90
  this.ctx.state.userInfo = userInfo;
90
91
  this.ctx.state.token = token;
91
92
  }
93
+
94
+ getId() {
95
+ const id = this.get('id');
96
+
97
+ if (id && (think.isString(id) || think.isNumber(id))) {
98
+ return id;
99
+ }
100
+
101
+ const last = decodeURIComponent(this.ctx.path.split('/').pop());
102
+
103
+ if (last !== this.resource && /^([a-z0-9]+,?)*$/i.test(last)) {
104
+ return last;
105
+ }
106
+
107
+ return '';
108
+ }
92
109
  };
package/src/logic/user.js CHANGED
@@ -1,6 +1,24 @@
1
1
  const Base = require('./base');
2
2
 
3
3
  module.exports = class extends Base {
4
+ getAction() {
5
+ const { userInfo } = this.ctx.state;
6
+ if (think.isEmpty(userInfo) || userInfo.type !== 'administrator') {
7
+ return this.fail();
8
+ }
9
+
10
+ this.rules = {
11
+ page: {
12
+ int: true,
13
+ default: 1,
14
+ },
15
+ pageSize: {
16
+ int: { max: 100 },
17
+ default: 10,
18
+ },
19
+ };
20
+ }
21
+
4
22
  /**
5
23
  * @api {POST} /user user register
6
24
  * @apiGroup User
@@ -30,9 +48,15 @@ module.exports = class extends Base {
30
48
  * @apiSuccess (200) {String} errmsg return error message if error
31
49
  */
32
50
  putAction() {
51
+ // you need login to update yourself profile
33
52
  const { userInfo } = this.ctx.state;
34
53
  if (think.isEmpty(userInfo)) {
35
54
  return this.fail();
36
55
  }
56
+
57
+ // you should be a administrator to update otherself info
58
+ if (this.id && userInfo.type !== 'administrator') {
59
+ return this.fail();
60
+ }
37
61
  }
38
62
  };
@@ -126,6 +126,88 @@ module.exports = class extends Base {
126
126
  return data;
127
127
  }
128
128
 
129
+ async _getCmtGroupByMailUserIdCache(key, where) {
130
+ if (this.tableName !== 'Comment' || key !== 'user_id_mail') {
131
+ return [];
132
+ }
133
+
134
+ const cacheTableName = `cache_group_count_${key}`;
135
+ const currentTableName = this.tableName;
136
+ this.tableName = cacheTableName;
137
+ const cacheData = await this.select({ _complex: where._complex });
138
+ this.tableName = currentTableName;
139
+ return cacheData;
140
+ }
141
+
142
+ async _setCmtGroupByMailUserIdCache(key, data) {
143
+ if (this.tableName !== 'Comment' || key !== 'user_id_mail') {
144
+ return;
145
+ }
146
+
147
+ const cacheTableName = `cache_group_count_${key}`;
148
+ const currentTableName = this.tableName;
149
+ this.tableName = cacheTableName;
150
+
151
+ await think.promiseAllQueue(
152
+ data.map((item) => {
153
+ if (item.user_id && !think.isString(item.user_id)) {
154
+ item.user_id = item.user_id.toString();
155
+ }
156
+ return this.add(item);
157
+ }),
158
+ 1
159
+ );
160
+ this.tableName = currentTableName;
161
+ }
162
+
163
+ async _updateCmtGroupByMailUserIdCache(data, method) {
164
+ if (this.tableName !== 'Comment') {
165
+ return;
166
+ }
167
+
168
+ if (!data.user_id && !data.mail) {
169
+ return;
170
+ }
171
+
172
+ const cacheTableName = `cache_group_count_user_id_mail`;
173
+ const cacheData = await this.select({
174
+ _complex: {
175
+ _logic: 'or',
176
+ user_id: think.isObject(data.user_id)
177
+ ? data.user_id.toString()
178
+ : data.user_id,
179
+ mail: data.mail,
180
+ },
181
+ });
182
+ if (think.isEmpty(data)) {
183
+ return;
184
+ }
185
+
186
+ let count = cacheData[0].count;
187
+ switch (method) {
188
+ case 'add':
189
+ if (data.status === 'approved') {
190
+ count += 1;
191
+ }
192
+ break;
193
+ case 'udpate_status':
194
+ if (data.status === 'approved') {
195
+ count += 1;
196
+ } else {
197
+ count -= 1;
198
+ }
199
+ break;
200
+ case 'delete':
201
+ count -= 1;
202
+ break;
203
+ }
204
+
205
+ const currentTableName = this.tableName;
206
+ this.tableName = cacheTableName;
207
+ await this.update({ count }, { objectId: cacheData[0].objectId });
208
+ this.tableName = currentTableName;
209
+ }
210
+
129
211
  async count(where = {}, options = {}) {
130
212
  const instance = this.where(this.tableName, where);
131
213
  if (!options.group) {
@@ -137,7 +219,19 @@ module.exports = class extends Base {
137
219
  });
138
220
  }
139
221
 
140
- // todo: query optimize
222
+ // get group count cache by group field where data
223
+ const cacheData = await this._getCmtGroupByMailUserIdCache(
224
+ options.group.join('_'),
225
+ where
226
+ );
227
+ const cacheDataMap = {};
228
+ for (let i = 0; i < cacheData.length; i++) {
229
+ const key = options.group
230
+ .map((item) => cacheData[i][item] || null)
231
+ .join('_');
232
+ cacheDataMap[key] = cacheData[i];
233
+ }
234
+
141
235
  const counts = [];
142
236
  const countsPromise = [];
143
237
  for (let i = 0; i < options.group.length; i++) {
@@ -152,6 +246,19 @@ module.exports = class extends Base {
152
246
  });
153
247
 
154
248
  for (let j = 0; j < where._complex[groupName][1].length; j++) {
249
+ const cacheKey = options.group
250
+ .map(
251
+ (item) =>
252
+ ({
253
+ ...groupFlatValue,
254
+ [groupName]: where._complex[groupName][1][j],
255
+ }[item] || null)
256
+ )
257
+ .join('_');
258
+ if (cacheDataMap[cacheKey]) {
259
+ continue;
260
+ }
261
+
155
262
  const groupWhere = {
156
263
  ...where,
157
264
  ...groupFlatValue,
@@ -172,8 +279,10 @@ module.exports = class extends Base {
172
279
  }
173
280
  }
174
281
 
175
- await think.promiseAllQueue(countsPromise, 3);
176
- return counts;
282
+ await think.promiseAllQueue(countsPromise, 1);
283
+ // cache data
284
+ await this._setCmtGroupByMailUserIdCache(options.group.join('_'), counts);
285
+ return [...cacheData, ...counts];
177
286
  }
178
287
 
179
288
  async add(
@@ -190,6 +299,7 @@ module.exports = class extends Base {
190
299
  instance.setACL(acl);
191
300
 
192
301
  const resp = await instance.save();
302
+ await this._updateCmtGroupByMailUserIdCache(data, 'add');
193
303
  return resp.toJSON();
194
304
  }
195
305
 
@@ -199,11 +309,16 @@ module.exports = class extends Base {
199
309
 
200
310
  return Promise.all(
201
311
  ret.map(async (item) => {
312
+ const _oldStatus = item.get('status');
202
313
  if (think.isFunction(data)) {
203
314
  item.set(data(item.toJSON()));
204
315
  } else {
205
316
  item.set(data);
206
317
  }
318
+ const _newStatus = item.get('status');
319
+ if (_newStatus && _oldStatus !== _newStatus) {
320
+ await this._updateCmtGroupByMailUserIdCache(data, 'update_status');
321
+ }
207
322
 
208
323
  const resp = await item.save();
209
324
  return resp.toJSON();
@@ -214,6 +329,7 @@ module.exports = class extends Base {
214
329
  async delete(where) {
215
330
  const instance = this.where(this.tableName, where);
216
331
  const data = await instance.find();
332
+ await this._updateCmtGroupByMailUserIdCache(data, 'delete');
217
333
 
218
334
  return AV.Object.destroyAll(data);
219
335
  }