@waline/vercel 1.36.5 → 1.38.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/development.js +1 -0
- package/index.js +4 -3
- package/package.json +2 -2
- package/src/config/adapter.js +8 -5
- package/src/config/config.js +10 -5
- package/src/controller/article.js +2 -2
- package/src/controller/comment.js +24 -21
- package/src/controller/oauth.js +7 -4
- package/src/controller/rest.js +1 -1
- package/src/controller/token.js +3 -1
- package/src/controller/user.js +30 -8
- package/src/controller/verification.js +3 -2
- package/src/extend/think.js +33 -19
- package/src/locales/de.json +19 -0
- package/src/locales/es.json +19 -0
- package/src/locales/fr.json +19 -0
- package/src/locales/index.js +18 -2
- package/src/locales/jp.json +19 -0
- package/src/locales/pt-BR.json +19 -0
- package/src/locales/ru.json +19 -0
- package/src/locales/vi-VN.json +19 -0
- package/src/logic/base.js +26 -24
- package/src/logic/comment.js +7 -4
- package/src/logic/db.js +1 -1
- package/src/logic/token.js +2 -2
- package/src/logic/user.js +25 -0
- package/src/middleware/dashboard.js +1 -0
- package/src/middleware/plugin.js +1 -1
- package/src/service/akismet.js +10 -3
- package/src/service/avatar.js +1 -1
- package/src/service/markdown/highlight.js +1 -1
- package/src/service/markdown/utils.js +5 -5
- package/src/service/markdown/xss.js +5 -10
- package/src/service/notify.js +56 -54
- package/src/service/storage/base.js +1 -1
- package/src/service/storage/cloudbase.js +9 -7
- package/src/service/storage/github.js +24 -18
- package/src/service/storage/leancloud.js +33 -26
- package/src/service/storage/mongodb.js +10 -6
- package/src/service/storage/mysql.js +4 -4
- package/src/service/storage/postgresql.js +5 -5
package/development.js
CHANGED
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const Application = require('thinkjs');
|
|
5
5
|
const Loader = require('thinkjs/lib/loader');
|
|
6
6
|
|
|
7
|
-
module.exports = function (configParams = {}) {
|
|
7
|
+
module.exports = function main(configParams = {}) {
|
|
8
8
|
const { env, ...config } = configParams;
|
|
9
9
|
|
|
10
10
|
const app = new Application({
|
|
@@ -20,10 +20,11 @@ module.exports = function (configParams = {}) {
|
|
|
20
20
|
|
|
21
21
|
loader.loadAll('worker');
|
|
22
22
|
|
|
23
|
+
// oxlint-disable-next-line func-names
|
|
23
24
|
return function (req, res) {
|
|
24
|
-
for (const
|
|
25
|
+
for (const key in config) {
|
|
25
26
|
// fix https://github.com/walinejs/waline/issues/2649 with alias model config name
|
|
26
|
-
think.config(
|
|
27
|
+
think.config(key === 'model' ? 'customModel' : key, config[key]);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
return think
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waline/vercel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "vercel server for waline comment system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"blog",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"@mdit/plugin-sup": "0.22.5-cjs.0",
|
|
30
30
|
"akismet": "^2.0.7",
|
|
31
31
|
"dompurify": "^3.3.1",
|
|
32
|
-
"dy-node-ip2region": "^1.0.1",
|
|
33
32
|
"fast-csv": "^5.0.5",
|
|
34
33
|
"form-data": "^4.0.5",
|
|
34
|
+
"ip2region": "^2.3.0",
|
|
35
35
|
"jsdom": "^19.0.0",
|
|
36
36
|
"jsonwebtoken": "^9.0.3",
|
|
37
37
|
"koa-compose": "^4.1.0",
|
package/src/config/adapter.js
CHANGED
|
@@ -6,9 +6,10 @@ const Postgresql = require('think-model-postgresql');
|
|
|
6
6
|
let Sqlite;
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
|
+
// oxlint-disable-next-line node/global-require
|
|
9
10
|
Sqlite = require('think-model-sqlite');
|
|
10
11
|
} catch (err) {
|
|
11
|
-
//
|
|
12
|
+
// oxlint-disable-next-line typescript/no-extraneous-class
|
|
12
13
|
Sqlite = class {};
|
|
13
14
|
console.log(err);
|
|
14
15
|
}
|
|
@@ -65,11 +66,11 @@ if (MONGO_AUTHSOURCE) mongoOpt.authSource = MONGO_AUTHSOURCE;
|
|
|
65
66
|
if (MONGO_DB) {
|
|
66
67
|
type = 'mongo';
|
|
67
68
|
for (const envKeys in process.env) {
|
|
68
|
-
if (
|
|
69
|
+
if (envKeys.includes('MONGO_OPT_')) {
|
|
69
70
|
const key = envKeys
|
|
70
71
|
.slice(10)
|
|
71
72
|
.toLocaleLowerCase()
|
|
72
|
-
.
|
|
73
|
+
.replaceAll(/_([a-z])/g, (_, b) => b.toUpperCase());
|
|
73
74
|
|
|
74
75
|
mongoOpt[key] = process.env[envKeys];
|
|
75
76
|
}
|
|
@@ -88,7 +89,9 @@ exports.model = {
|
|
|
88
89
|
type,
|
|
89
90
|
common: {
|
|
90
91
|
logSql: true,
|
|
91
|
-
logger: (msg) =>
|
|
92
|
+
logger: (msg) => {
|
|
93
|
+
think.logger.info(msg);
|
|
94
|
+
},
|
|
92
95
|
},
|
|
93
96
|
|
|
94
97
|
mongo: {
|
|
@@ -114,7 +117,7 @@ exports.model = {
|
|
|
114
117
|
connectionLimit: 1,
|
|
115
118
|
prefix: PG_PREFIX || POSTGRES_PREFIX || 'wl_',
|
|
116
119
|
ssl:
|
|
117
|
-
(PG_SSL || POSTGRES_SSL)
|
|
120
|
+
(PG_SSL || POSTGRES_SSL) === 'true' || POSTGRES_URL?.includes('sslmode=require')
|
|
118
121
|
? {
|
|
119
122
|
rejectUnauthorized: false,
|
|
120
123
|
}
|
package/src/config/config.js
CHANGED
|
@@ -48,11 +48,12 @@ const {
|
|
|
48
48
|
COMMENT_AUDIT,
|
|
49
49
|
} = process.env;
|
|
50
50
|
|
|
51
|
-
let storage =
|
|
52
|
-
let jwtKey = JWT_TOKEN
|
|
51
|
+
let storage = null;
|
|
52
|
+
let jwtKey = JWT_TOKEN;
|
|
53
53
|
|
|
54
54
|
if (LEAN_KEY) {
|
|
55
55
|
storage = 'leancloud';
|
|
56
|
+
jwtKey = jwtKey || LEAN_KEY;
|
|
56
57
|
} else if (MONGO_DB) {
|
|
57
58
|
storage = 'mongodb';
|
|
58
59
|
jwtKey = jwtKey || MONGO_PASSWORD;
|
|
@@ -75,6 +76,10 @@ if (LEAN_KEY) {
|
|
|
75
76
|
jwtKey = jwtKey || TENCENTCLOUD_SECRETKEY || TCB_KEY || TCB_ENV;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
if (storage === null) {
|
|
80
|
+
throw new Error('No valid storage found. Please check your environment variables.');
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
if (think.env === 'cloudbase' && storage === 'sqlite') {
|
|
79
84
|
throw new Error("You can't use SQLite in CloudBase platform.");
|
|
80
85
|
}
|
|
@@ -100,7 +105,7 @@ if (isFalse(MARKDOWN_HIGHLIGHT)) markdown.config.highlight = false;
|
|
|
100
105
|
let avatarProxy = '';
|
|
101
106
|
|
|
102
107
|
if (AVATAR_PROXY) {
|
|
103
|
-
avatarProxy =
|
|
108
|
+
avatarProxy = isFalse(AVATAR_PROXY) ? '' : AVATAR_PROXY;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
const oauthUrl = OAUTH_URL || 'https://oauth.lithub.cc';
|
|
@@ -111,10 +116,10 @@ module.exports = {
|
|
|
111
116
|
jwtKey,
|
|
112
117
|
forbiddenWords,
|
|
113
118
|
disallowIPList: [],
|
|
114
|
-
secureDomains: SECURE_DOMAINS ? SECURE_DOMAINS.split(/\s*,\s*/) :
|
|
119
|
+
secureDomains: SECURE_DOMAINS ? SECURE_DOMAINS.split(/\s*,\s*/) : null,
|
|
115
120
|
disableUserAgent: DISABLE_USERAGENT && !isFalse(DISABLE_USERAGENT),
|
|
116
121
|
disableRegion: DISABLE_REGION && !isFalse(DISABLE_REGION),
|
|
117
|
-
levels: !LEVELS || isFalse(LEVELS) ? false : LEVELS.split(/\s*,\s*/).map(
|
|
122
|
+
levels: !LEVELS || isFalse(LEVELS) ? false : LEVELS.split(/\s*,\s*/).map(Number),
|
|
118
123
|
|
|
119
124
|
audit: COMMENT_AUDIT && !isFalse(COMMENT_AUDIT),
|
|
120
125
|
avatarProxy,
|
|
@@ -11,14 +11,14 @@ module.exports = class extends BaseRest {
|
|
|
11
11
|
const { deprecated } = this.ctx.state;
|
|
12
12
|
|
|
13
13
|
// path is required
|
|
14
|
-
if (!Array.isArray(path) ||
|
|
14
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
15
15
|
return this.jsonOrSuccess(0);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const resp = await this.modelInstance.select({ url: ['IN', path] });
|
|
19
19
|
|
|
20
20
|
if (think.isEmpty(resp)) {
|
|
21
|
-
const counters =
|
|
21
|
+
const counters = Array(path.length).fill(
|
|
22
22
|
type.length === 1 && deprecated
|
|
23
23
|
? 0
|
|
24
24
|
: type.reduce((o, field) => {
|
|
@@ -4,12 +4,13 @@ const { getMarkdownParser } = require('../service/markdown/index.js');
|
|
|
4
4
|
|
|
5
5
|
const markdownParser = getMarkdownParser();
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// oxlint-disable-next-line max-statements
|
|
8
|
+
const formatCmt = async (
|
|
8
9
|
{ ua, ip, ...comment },
|
|
9
10
|
users = [],
|
|
10
11
|
{ avatarProxy, deprecated },
|
|
11
12
|
loginUser,
|
|
12
|
-
) {
|
|
13
|
+
) => {
|
|
13
14
|
ua = think.uaParser(ua);
|
|
14
15
|
if (!think.config('disableUserAgent')) {
|
|
15
16
|
comment.browser = `${ua.browser.name || ''}${(ua.browser.version || '')
|
|
@@ -33,7 +34,7 @@ async function formatCmt(
|
|
|
33
34
|
|
|
34
35
|
comment.avatar =
|
|
35
36
|
avatarProxy && !avatarUrl.includes(avatarProxy)
|
|
36
|
-
? avatarProxy
|
|
37
|
+
? `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`
|
|
37
38
|
: avatarUrl;
|
|
38
39
|
|
|
39
40
|
const isAdmin = loginUser && loginUser.type === 'administrator';
|
|
@@ -67,7 +68,7 @@ async function formatCmt(
|
|
|
67
68
|
delete comment.updatedAt;
|
|
68
69
|
|
|
69
70
|
return comment;
|
|
70
|
-
}
|
|
71
|
+
};
|
|
71
72
|
|
|
72
73
|
module.exports = class extends BaseRest {
|
|
73
74
|
constructor(ctx) {
|
|
@@ -79,12 +80,12 @@ module.exports = class extends BaseRest {
|
|
|
79
80
|
const { type } = this.get();
|
|
80
81
|
|
|
81
82
|
const fnMap = {
|
|
82
|
-
recent: this
|
|
83
|
-
count: this
|
|
84
|
-
list: this
|
|
83
|
+
recent: this['getRecentCommentList'],
|
|
84
|
+
count: this['getCommentCount'],
|
|
85
|
+
list: this['getAdminCommentList'],
|
|
85
86
|
};
|
|
86
87
|
|
|
87
|
-
const fn = fnMap[type] || this
|
|
88
|
+
const fn = fnMap[type] || this['getCommentList'];
|
|
88
89
|
const data = await fn.call(this);
|
|
89
90
|
|
|
90
91
|
return this.jsonOrSuccess(data);
|
|
@@ -122,7 +123,7 @@ module.exports = class extends BaseRest {
|
|
|
122
123
|
|
|
123
124
|
if (
|
|
124
125
|
think.isArray(disallowIPList) &&
|
|
125
|
-
disallowIPList.length &&
|
|
126
|
+
disallowIPList.length > 0 &&
|
|
126
127
|
disallowIPList.includes(data.ip)
|
|
127
128
|
) {
|
|
128
129
|
think.logger.debug(`Comment IP ${data.ip} is in disallowIPList`);
|
|
@@ -171,7 +172,9 @@ module.exports = class extends BaseRest {
|
|
|
171
172
|
think.logger.debug(`Comment initial status is ${data.status}`);
|
|
172
173
|
|
|
173
174
|
if (data.status === 'approved') {
|
|
174
|
-
const spam = await akismet(data, this.ctx.serverURL).catch((err) =>
|
|
175
|
+
const spam = await akismet(data, this.ctx.serverURL).catch((err) => {
|
|
176
|
+
console.log(err);
|
|
177
|
+
}); // ignore akismet error
|
|
175
178
|
|
|
176
179
|
if (spam === true) {
|
|
177
180
|
data.status = 'spam';
|
|
@@ -272,7 +275,7 @@ module.exports = class extends BaseRest {
|
|
|
272
275
|
async putAction() {
|
|
273
276
|
const { userInfo } = this.ctx.state;
|
|
274
277
|
const isAdmin = userInfo.type === 'administrator';
|
|
275
|
-
|
|
278
|
+
const data = isAdmin ? this.post() : this.post('comment,like');
|
|
276
279
|
let oldData = await this.modelInstance.select({ objectId: this.id });
|
|
277
280
|
|
|
278
281
|
if (think.isEmpty(oldData) || think.isEmpty(data)) {
|
|
@@ -473,10 +476,10 @@ module.exports = class extends BaseRest {
|
|
|
473
476
|
}
|
|
474
477
|
|
|
475
478
|
const userModel = this.getModel('Users');
|
|
476
|
-
const user_ids =
|
|
479
|
+
const user_ids = [...new Set(comments.map(({ user_id }) => user_id).filter((v) => v))];
|
|
477
480
|
let users = [];
|
|
478
481
|
|
|
479
|
-
if (user_ids.length) {
|
|
482
|
+
if (user_ids.length > 0) {
|
|
480
483
|
users = await userModel.select(
|
|
481
484
|
{ objectId: ['IN', user_ids] },
|
|
482
485
|
{
|
|
@@ -491,12 +494,12 @@ module.exports = class extends BaseRest {
|
|
|
491
494
|
_complex: {},
|
|
492
495
|
};
|
|
493
496
|
|
|
494
|
-
if (user_ids.length) {
|
|
497
|
+
if (user_ids.length > 0) {
|
|
495
498
|
countWhere._complex.user_id = ['IN', user_ids];
|
|
496
499
|
}
|
|
497
|
-
const mails =
|
|
500
|
+
const mails = [...new Set(comments.map(({ mail }) => mail).filter((v) => v))];
|
|
498
501
|
|
|
499
|
-
if (mails.length) {
|
|
502
|
+
if (mails.length > 0) {
|
|
500
503
|
countWhere._complex.mail = ['IN', mails];
|
|
501
504
|
}
|
|
502
505
|
if (!think.isEmpty(countWhere._complex)) {
|
|
@@ -610,11 +613,11 @@ module.exports = class extends BaseRest {
|
|
|
610
613
|
});
|
|
611
614
|
|
|
612
615
|
const userModel = this.getModel('Users');
|
|
613
|
-
const user_ids =
|
|
616
|
+
const user_ids = [...new Set(comments.map(({ user_id }) => user_id).filter((v) => v))];
|
|
614
617
|
|
|
615
618
|
let users = [];
|
|
616
619
|
|
|
617
|
-
if (user_ids.length) {
|
|
620
|
+
if (user_ids.length > 0) {
|
|
618
621
|
users = await userModel.select(
|
|
619
622
|
{ objectId: ['IN', user_ids] },
|
|
620
623
|
{
|
|
@@ -679,11 +682,11 @@ module.exports = class extends BaseRest {
|
|
|
679
682
|
});
|
|
680
683
|
|
|
681
684
|
const userModel = this.getModel('Users');
|
|
682
|
-
const user_ids =
|
|
685
|
+
const user_ids = [...new Set(comments.map(({ user_id }) => user_id).filter((v) => v))];
|
|
683
686
|
|
|
684
687
|
let users = [];
|
|
685
688
|
|
|
686
|
-
if (user_ids.length) {
|
|
689
|
+
if (user_ids.length > 0) {
|
|
687
690
|
users = await userModel.select(
|
|
688
691
|
{ objectId: ['IN', user_ids] },
|
|
689
692
|
{
|
|
@@ -707,7 +710,7 @@ module.exports = class extends BaseRest {
|
|
|
707
710
|
async getCommentCount() {
|
|
708
711
|
const { url } = this.get();
|
|
709
712
|
const { userInfo } = this.ctx.state;
|
|
710
|
-
const where = Array.isArray(url) && url.length ? { url: ['IN', url] } : {};
|
|
713
|
+
const where = Array.isArray(url) && url.length > 0 ? { url: ['IN', url] } : {};
|
|
711
714
|
|
|
712
715
|
if (think.isEmpty(userInfo)) {
|
|
713
716
|
where.status = ['NOT IN', ['waiting', 'spam']];
|
package/src/controller/oauth.js
CHANGED
|
@@ -19,12 +19,13 @@ module.exports = class extends think.Controller {
|
|
|
19
19
|
type,
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
this.redirect(
|
|
23
23
|
think.buildUrl(`${oauthUrl}/${type}`, {
|
|
24
24
|
redirect: redirectUrl,
|
|
25
25
|
state: this.ctx.state.token || '',
|
|
26
26
|
}),
|
|
27
27
|
);
|
|
28
|
+
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -64,7 +65,8 @@ module.exports = class extends think.Controller {
|
|
|
64
65
|
const token = jwt.sign(userBySocial[0].objectId, this.config('jwtKey'));
|
|
65
66
|
|
|
66
67
|
if (redirect) {
|
|
67
|
-
|
|
68
|
+
this.redirect(think.buildUrl(redirect, { token }));
|
|
69
|
+
return;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
return this.success();
|
|
@@ -84,7 +86,8 @@ module.exports = class extends think.Controller {
|
|
|
84
86
|
objectId: current.objectId,
|
|
85
87
|
});
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
this.redirect('/ui/profile');
|
|
90
|
+
return;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
// when user has not login, then we create account by the social type!
|
|
@@ -108,6 +111,6 @@ module.exports = class extends think.Controller {
|
|
|
108
111
|
// and then generate token!
|
|
109
112
|
const token = jwt.sign(user.objectId, this.config('jwtKey'));
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
this.redirect(redirect + (redirect.includes('?') ? '&' : '?') + 'token=' + token);
|
|
112
115
|
}
|
|
113
116
|
};
|
package/src/controller/rest.js
CHANGED
|
@@ -21,7 +21,7 @@ module.exports = class extends think.Controller {
|
|
|
21
21
|
const filename = this.__filename || __filename;
|
|
22
22
|
const last = filename.lastIndexOf(path.sep);
|
|
23
23
|
|
|
24
|
-
return filename.
|
|
24
|
+
return filename.slice(last + 1, filename.length - last - 4);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
getId() {
|
package/src/controller/token.js
CHANGED
|
@@ -17,7 +17,9 @@ module.exports = class extends BaseRest {
|
|
|
17
17
|
const { email, password, code } = this.post();
|
|
18
18
|
const user = await this.modelInstance.select({ email });
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const isVerifyUser = /^verify:/i.test(user?.[0]?.type);
|
|
21
|
+
const isBannedUser = user?.[0]?.type === 'banned';
|
|
22
|
+
if (think.isEmpty(user) || isVerifyUser || isBannedUser) {
|
|
21
23
|
return this.fail();
|
|
22
24
|
}
|
|
23
25
|
|
package/src/controller/user.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const BaseRest = require('./rest.js');
|
|
2
2
|
|
|
3
|
-
module.exports = class extends BaseRest {
|
|
3
|
+
module.exports = class UserController extends BaseRest {
|
|
4
4
|
constructor(...args) {
|
|
5
5
|
super(...args);
|
|
6
6
|
this.modelInstance = this.getModel('Users');
|
|
@@ -80,7 +80,7 @@ module.exports = class extends BaseRest {
|
|
|
80
80
|
|
|
81
81
|
try {
|
|
82
82
|
const notify = this.service('notify', this);
|
|
83
|
-
const apiUrl = think.buildUrl(this.ctx.serverURL
|
|
83
|
+
const apiUrl = think.buildUrl(`${this.ctx.serverURL}/verification`, {
|
|
84
84
|
token,
|
|
85
85
|
email: data.email,
|
|
86
86
|
});
|
|
@@ -179,6 +179,28 @@ module.exports = class extends BaseRest {
|
|
|
179
179
|
return this.success();
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
async deleteAction() {
|
|
183
|
+
const users = await this.modelInstance.select({
|
|
184
|
+
objectId: this.id,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (think.isEmpty(users)) {
|
|
188
|
+
return this.fail();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const user = users[0];
|
|
192
|
+
const isVerifyUser = /^verify:/i.test(user.type);
|
|
193
|
+
|
|
194
|
+
if (isVerifyUser) {
|
|
195
|
+
await this.modelInstance.delete({ objectId: this.id });
|
|
196
|
+
} else {
|
|
197
|
+
await this.modelInstance.update({ type: 'banned' }, { objectId: this.id });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return this.success();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// oxlint-disable-next-line max-statements
|
|
182
204
|
async getUsersListByCount() {
|
|
183
205
|
const { pageSize } = this.get();
|
|
184
206
|
const commentModel = this.getModel('Comment');
|
|
@@ -196,9 +218,9 @@ module.exports = class extends BaseRest {
|
|
|
196
218
|
|
|
197
219
|
const userIds = counts.filter(({ user_id }) => user_id).map(({ user_id }) => user_id);
|
|
198
220
|
|
|
199
|
-
|
|
221
|
+
const usersMap = {};
|
|
200
222
|
|
|
201
|
-
if (userIds.length) {
|
|
223
|
+
if (userIds.length > 0) {
|
|
202
224
|
const users = await this.modelInstance.select({
|
|
203
225
|
objectId: ['IN', userIds],
|
|
204
226
|
});
|
|
@@ -220,7 +242,7 @@ module.exports = class extends BaseRest {
|
|
|
220
242
|
let level = 0;
|
|
221
243
|
|
|
222
244
|
if (user.count) {
|
|
223
|
-
const _level = think.findLastIndex(this.config('levels'), (
|
|
245
|
+
const _level = think.findLastIndex(this.config('levels'), (level) => level <= user.count);
|
|
224
246
|
|
|
225
247
|
if (_level !== -1) {
|
|
226
248
|
level = _level;
|
|
@@ -233,7 +255,7 @@ module.exports = class extends BaseRest {
|
|
|
233
255
|
const { display_name: nick, url: link, avatar: avatarUrl, label } = users[count.user_id];
|
|
234
256
|
const avatar =
|
|
235
257
|
avatarProxy && !avatarUrl.includes(avatarProxy)
|
|
236
|
-
? avatarProxy
|
|
258
|
+
? `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`
|
|
237
259
|
: avatarUrl;
|
|
238
260
|
|
|
239
261
|
Object.assign(user, { nick, link, avatar, label });
|
|
@@ -246,7 +268,7 @@ module.exports = class extends BaseRest {
|
|
|
246
268
|
if (think.isEmpty(comments)) {
|
|
247
269
|
continue;
|
|
248
270
|
}
|
|
249
|
-
const comment = comments
|
|
271
|
+
const [comment] = comments;
|
|
250
272
|
|
|
251
273
|
if (think.isEmpty(comment)) {
|
|
252
274
|
continue;
|
|
@@ -255,7 +277,7 @@ module.exports = class extends BaseRest {
|
|
|
255
277
|
const avatarUrl = await think.service('avatar').stringify(comment);
|
|
256
278
|
const avatar =
|
|
257
279
|
avatarProxy && !avatarUrl.includes(avatarProxy)
|
|
258
|
-
? avatarProxy
|
|
280
|
+
? `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`
|
|
259
281
|
: avatarUrl;
|
|
260
282
|
|
|
261
283
|
Object.assign(user, { nick, link, avatar });
|
|
@@ -21,10 +21,11 @@ module.exports = class extends BaseRest {
|
|
|
21
21
|
return this.fail(this.locale('USER_REGISTERED'));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
if (token === match[1] && Date.now() < parseInt(match[2])) {
|
|
24
|
+
if (token === match[1] && Date.now() < Number.parseInt(match[2])) {
|
|
25
25
|
await this.modelInstance.update({ type: 'guest' }, { email });
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
this.redirect('/ui/login');
|
|
28
|
+
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
return this.fail(this.locale('TOKEN_EXPIRED'));
|
package/src/extend/think.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
const
|
|
2
|
-
const helper = require('think-helper');
|
|
1
|
+
const IP2Region = require('ip2region').default;
|
|
3
2
|
const parser = require('ua-parser-js');
|
|
4
3
|
|
|
5
4
|
const preventMessage = 'PREVENT_NEXT_PROCESS';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
// Cached IP2Region instance using IIFE closure pattern
|
|
7
|
+
// Instance is created on first access and reused for all subsequent calls
|
|
8
|
+
const getIP2RegionInstance = (() => {
|
|
9
|
+
let instance = null;
|
|
10
|
+
|
|
11
|
+
return () => {
|
|
12
|
+
if (!instance) {
|
|
13
|
+
instance = new IP2Region({
|
|
14
|
+
ipv4db: process.env.IP2REGION_DB_V4 || process.env.IP2REGION_DB,
|
|
15
|
+
ipv6db: process.env.IP2REGION_DB_V6,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return instance;
|
|
20
|
+
};
|
|
21
|
+
})();
|
|
8
22
|
|
|
9
23
|
const OS_VERSION_MAP = {
|
|
10
24
|
Windows: {
|
|
@@ -34,15 +48,17 @@ module.exports = {
|
|
|
34
48
|
},
|
|
35
49
|
promiseAllQueue(promises, taskNum) {
|
|
36
50
|
return new Promise((resolve, reject) => {
|
|
37
|
-
if (
|
|
38
|
-
|
|
51
|
+
if (promises.length === 0) {
|
|
52
|
+
resolve();
|
|
53
|
+
|
|
54
|
+
return;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
const ret = [];
|
|
42
58
|
let index = 0;
|
|
43
59
|
let count = 0;
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
const runTask = () => {
|
|
46
62
|
const idx = index;
|
|
47
63
|
|
|
48
64
|
index += 1;
|
|
@@ -59,7 +75,7 @@ module.exports = {
|
|
|
59
75
|
|
|
60
76
|
return runTask();
|
|
61
77
|
}, reject);
|
|
62
|
-
}
|
|
78
|
+
};
|
|
63
79
|
|
|
64
80
|
for (let i = 0; i < taskNum; i++) {
|
|
65
81
|
runTask();
|
|
@@ -67,19 +83,17 @@ module.exports = {
|
|
|
67
83
|
});
|
|
68
84
|
},
|
|
69
85
|
async ip2region(ip, { depth = 1 }) {
|
|
70
|
-
if (!ip
|
|
86
|
+
if (!ip) return '';
|
|
71
87
|
|
|
72
88
|
try {
|
|
73
|
-
const
|
|
74
|
-
const result = await search(ip);
|
|
89
|
+
const res = getIP2RegionInstance().search(ip);
|
|
75
90
|
|
|
76
|
-
if (!
|
|
91
|
+
if (!res) {
|
|
77
92
|
return '';
|
|
78
93
|
}
|
|
79
|
-
const { region } = result;
|
|
80
|
-
const [, , province, city, isp] = region.split('|');
|
|
81
|
-
const address = Array.from(new Set([province, city, isp].filter((v) => v)));
|
|
82
94
|
|
|
95
|
+
const { province, city, isp } = res;
|
|
96
|
+
const address = [...new Set([province, city, isp].filter(Boolean))];
|
|
83
97
|
return address.slice(0, depth).join(' ');
|
|
84
98
|
} catch (err) {
|
|
85
99
|
console.log(err);
|
|
@@ -104,7 +118,7 @@ module.exports = {
|
|
|
104
118
|
return defaultLevel;
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
const level = think.findLastIndex(levels, (
|
|
121
|
+
const level = think.findLastIndex(levels, (level) => level <= val);
|
|
108
122
|
|
|
109
123
|
return level === -1 ? defaultLevel : level;
|
|
110
124
|
},
|
|
@@ -139,7 +153,7 @@ module.exports = {
|
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
if (think.isArray(middleware)) {
|
|
142
|
-
return middleware.filter((
|
|
156
|
+
return middleware.filter((middleware) => think.isFunction(middleware));
|
|
143
157
|
}
|
|
144
158
|
});
|
|
145
159
|
|
|
@@ -147,8 +161,8 @@ module.exports = {
|
|
|
147
161
|
},
|
|
148
162
|
getPluginHook(hookName) {
|
|
149
163
|
return think
|
|
150
|
-
.pluginMap('hooks', (hook) => (think.isFunction(hook[hookName]) ? hook[hookName] :
|
|
151
|
-
.filter(
|
|
164
|
+
.pluginMap('hooks', (hook) => (think.isFunction(hook[hookName]) ? hook[hookName] : null))
|
|
165
|
+
.filter(Boolean);
|
|
152
166
|
},
|
|
153
167
|
buildUrl(path, query = {}) {
|
|
154
168
|
const notEmptyQuery = {};
|
|
@@ -165,7 +179,7 @@ module.exports = {
|
|
|
165
179
|
let destUrl = path;
|
|
166
180
|
|
|
167
181
|
if (destUrl && notEmptyQueryStr) {
|
|
168
|
-
destUrl += destUrl.
|
|
182
|
+
destUrl += destUrl.includes('?') ? '&' : '?';
|
|
169
183
|
}
|
|
170
184
|
if (notEmptyQueryStr) {
|
|
171
185
|
destUrl += notEmptyQueryStr;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "Dateiformat wird nicht unterstützt",
|
|
3
|
+
"USER_EXIST": "Benutzer existiert bereits",
|
|
4
|
+
"USER_NOT_EXIST": "Benutzer existiert nicht",
|
|
5
|
+
"USER_REGISTERED": "Benutzer bereits registriert",
|
|
6
|
+
"TOKEN_EXPIRED": "Token ist abgelaufen",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "Fehler bei der Zwei-Faktor-Authentifizierung",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】 Registrierungsbestätigung",
|
|
9
|
+
"Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "Bitte klicken Sie auf <a href=\"{{url}}\">{{url}}</a>, um die Registrierung zu bestätigen. Der Link ist 1 Stunde gültig. Falls Sie sich nicht registriert haben, ignorieren Sie diese E-Mail.",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "【{{name | safe}}】 Passwort zurücksetzen",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Bitte klicken Sie auf <a href=\"{{url}}\">{{url}}</a>, um sich anzumelden und Ihr Passwort so schnell wie möglich zu ändern!",
|
|
12
|
+
"Duplicate Content": "Doppelter Inhalt gesendet",
|
|
13
|
+
"Comment too fast": "Sie kommentieren zu schnell, bitte warten Sie einen Moment!",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Fehler beim Senden der Registrierungsbestätigung. Bitte {%- if isAdmin -%}überprüfen Sie Ihre Mail-Konfiguration{%- else -%}prüfen Sie Ihre E-Mail-Adresse und kontaktieren Sie den Administrator{%- endif -%}.",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},Sie haben eine Antwort auf ' {{site.name | safe}} ' erhalten",
|
|
16
|
+
"MAIL_TEMPLATE": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Ihr Kommentar auf <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> hat eine neue Antwort </h2>{{parent.nick}}, Sie haben kommentiert: <div style='padding:0 12px 0 12px;margin-top:18px'> <div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{parent.comment | safe}}</div><p><strong>{{self.nick}}</strong> hat geantwortet:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Sie können die vollständige Antwort ansehen unter <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>hier</a> und besuchen Sie erneut <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "Neue Kommentare auf {{site.name | safe}}",
|
|
18
|
+
"MAIL_TEMPLATE_ADMIN": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Ihr Artikel auf <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> hat neue Kommentare </h2> <p><strong>{{self.nick}}</strong> hat geantwortet:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Sie können die Antwort vollständig ansehen unter <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>hier</a></p><br/> </div>"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "Formato de archivo no compatible",
|
|
3
|
+
"USER_EXIST": "El usuario ya existe",
|
|
4
|
+
"USER_NOT_EXIST": "El usuario no existe",
|
|
5
|
+
"USER_REGISTERED": "Usuario ya registrado",
|
|
6
|
+
"TOKEN_EXPIRED": "El token ha expirado",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "Error de autenticación en dos pasos",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】Correo de confirmación de registro",
|
|
9
|
+
"Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "Por favor haga clic en <a href=\"{{url}}\">{{url}}</a> para confirmar el registro. El enlace es válido por 1 hora. Si no se ha registrado, ignore este correo.",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "【{{name | safe}}】Restablecer contraseña",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Por favor haga clic en <a href=\"{{url}}\">{{url}}</a> para iniciar sesión y cambiar su contraseña lo antes posible!",
|
|
12
|
+
"Duplicate Content": "Contenido duplicado enviado",
|
|
13
|
+
"Comment too fast": "Comentarios muy rápidos, por favor espere un momento!",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Error al enviar el correo de confirmación. Por favor {%- if isAdmin -%}verifique su configuración de correo{%- else -%}verifique su dirección de correo y contacte al administrador{%- endif -%}.",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},Su comentario en '{{site.name | safe}}' ha recibido una respuesta",
|
|
16
|
+
"MAIL_TEMPLATE": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Su comentario en <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> tiene una nueva respuesta </h2>{{parent.nick}}, usted comentó: <div style='padding:0 12px 0 12px;margin-top:18px'> <div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{parent.comment | safe}}</div><p><strong>{{self.nick}}</strong> respondió:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Puedes ver la respuesta completa en <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>aquí</a>, y visita de nuevo <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "Nuevo comentario en {{site.name | safe}}",
|
|
18
|
+
"MAIL_TEMPLATE_ADMIN": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Su artículo en <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> tiene nuevos comentarios </h2> <p><strong>{{self.nick}}</strong> dijo:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Puedes ver la respuesta completa en <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>aquí</a></p><br/> </div>"
|
|
19
|
+
}
|