@waline/vercel 1.26.3 → 1.26.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.
- package/dist/404.html +39 -0
- package/dist/500.html +275 -0
- package/dist/index.js +58501 -0
- package/dist/package.json +54 -0
- package/dist/src/config/adapter.js +170 -0
- package/dist/src/config/config.js +134 -0
- package/dist/src/config/extend.js +38 -0
- package/dist/src/config/middleware.js +66 -0
- package/dist/src/config/router.js +1 -0
- package/dist/src/controller/article.js +91 -0
- package/dist/src/controller/comment.js +758 -0
- package/dist/src/controller/db.js +71 -0
- package/dist/src/controller/index.js +36 -0
- package/dist/src/controller/oauth.js +136 -0
- package/dist/src/controller/rest.js +60 -0
- package/dist/src/controller/token/2fa.js +66 -0
- package/dist/src/controller/token.js +75 -0
- package/dist/src/controller/user/password.js +52 -0
- package/dist/src/controller/user.js +289 -0
- package/dist/src/controller/verification.js +35 -0
- package/dist/src/extend/controller.js +25 -0
- package/dist/src/extend/think.js +84 -0
- package/dist/src/locales/en.json +19 -0
- package/dist/src/locales/index.js +12 -0
- package/dist/src/locales/zh-CN.json +19 -0
- package/dist/src/locales/zh-TW.json +19 -0
- package/dist/src/logic/article.js +27 -0
- package/dist/src/logic/base.js +164 -0
- package/dist/src/logic/comment.js +317 -0
- package/dist/src/logic/db.js +81 -0
- package/dist/src/logic/oauth.js +10 -0
- package/dist/src/logic/token/2fa.js +28 -0
- package/dist/src/logic/token.js +53 -0
- package/dist/src/logic/user/password.js +11 -0
- package/dist/src/logic/user.js +117 -0
- package/dist/src/middleware/dashboard.js +23 -0
- package/dist/src/middleware/version.js +6 -0
- package/dist/src/service/akismet.js +41 -0
- package/dist/src/service/avatar.js +35 -0
- package/dist/src/service/markdown/highlight.js +32 -0
- package/dist/src/service/markdown/index.js +63 -0
- package/dist/src/service/markdown/katex.js +49 -0
- package/dist/src/service/markdown/mathCommon.js +156 -0
- package/dist/src/service/markdown/mathjax.js +78 -0
- package/dist/src/service/markdown/utils.js +11 -0
- package/dist/src/service/markdown/xss.js +44 -0
- package/dist/src/service/notify.js +537 -0
- package/dist/src/service/storage/base.js +31 -0
- package/dist/src/service/storage/cloudbase.js +221 -0
- package/dist/src/service/storage/deta.js +307 -0
- package/dist/src/service/storage/github.js +377 -0
- package/dist/src/service/storage/leancloud.js +430 -0
- package/dist/src/service/storage/mongodb.js +179 -0
- package/dist/src/service/storage/mysql.js +123 -0
- package/dist/src/service/storage/postgresql.js +84 -0
- package/dist/src/service/storage/sqlite.js +11 -0
- package/dist/src/service/storage/tidb.js +3 -0
- package/package.json +1 -1
- package/src/controller/comment.js +1 -2
- package/src/extend/think.js +19 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const { PasswordHash } = require('phpass');
|
|
2
|
+
const BaseRest = require('./rest');
|
|
3
|
+
|
|
4
|
+
module.exports = class extends BaseRest {
|
|
5
|
+
constructor(...args) {
|
|
6
|
+
super(...args);
|
|
7
|
+
this.modelInstance = this.service(
|
|
8
|
+
`storage/${this.config('storage')}`,
|
|
9
|
+
'Users'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getAction() {
|
|
14
|
+
const { page, pageSize, email } = this.get();
|
|
15
|
+
const { userInfo } = this.ctx.state;
|
|
16
|
+
|
|
17
|
+
if (think.isEmpty(userInfo) || userInfo.type !== 'administrator') {
|
|
18
|
+
const users = await this.getUsersListByCount();
|
|
19
|
+
|
|
20
|
+
return this.success(users);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (email) {
|
|
24
|
+
const user = await this.modelInstance.select({ email });
|
|
25
|
+
|
|
26
|
+
if (think.isEmpty(user)) {
|
|
27
|
+
return this.success();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return this.success(user[0]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const count = await this.modelInstance.count({});
|
|
34
|
+
const users = await this.modelInstance.select(
|
|
35
|
+
{},
|
|
36
|
+
{
|
|
37
|
+
desc: 'createdAt',
|
|
38
|
+
limit: pageSize,
|
|
39
|
+
offset: Math.max((page - 1) * pageSize, 0),
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return this.success({
|
|
44
|
+
page,
|
|
45
|
+
totalPages: Math.ceil(count / pageSize),
|
|
46
|
+
pageSize,
|
|
47
|
+
data: users,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async postAction() {
|
|
52
|
+
const data = this.post();
|
|
53
|
+
const resp = await this.modelInstance.select({
|
|
54
|
+
email: data.email,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
!think.isEmpty(resp) &&
|
|
59
|
+
['administrator', 'guest'].includes(resp[0].type)
|
|
60
|
+
) {
|
|
61
|
+
return this.fail(this.locale('USER_EXIST'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const count = await this.modelInstance.count();
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
SMTP_HOST,
|
|
68
|
+
SMTP_SERVICE,
|
|
69
|
+
SENDER_EMAIL,
|
|
70
|
+
SENDER_NAME,
|
|
71
|
+
SMTP_USER,
|
|
72
|
+
SITE_NAME,
|
|
73
|
+
} = process.env;
|
|
74
|
+
const hasMailService = SMTP_HOST || SMTP_SERVICE;
|
|
75
|
+
|
|
76
|
+
const token = Array.from({ length: 4 }, () =>
|
|
77
|
+
Math.round(Math.random() * 9)
|
|
78
|
+
).join('');
|
|
79
|
+
const normalType = hasMailService
|
|
80
|
+
? `verify:${token}:${Date.now() + 1 * 60 * 60 * 1000}`
|
|
81
|
+
: 'guest';
|
|
82
|
+
|
|
83
|
+
data.password = new PasswordHash().hashPassword(data.password);
|
|
84
|
+
data.type = think.isEmpty(count) ? 'administrator' : normalType;
|
|
85
|
+
|
|
86
|
+
if (think.isEmpty(resp)) {
|
|
87
|
+
await this.modelInstance.add(data);
|
|
88
|
+
} else {
|
|
89
|
+
await this.modelInstance.update(data, { email: data.email });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!/^verify:/i.test(data.type)) {
|
|
93
|
+
return this.success();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const notify = this.service('notify', this);
|
|
98
|
+
const apiUrl =
|
|
99
|
+
this.ctx.serverURL +
|
|
100
|
+
'/verification?' +
|
|
101
|
+
new URLSearchParams({ token, email: data.email }).toString();
|
|
102
|
+
|
|
103
|
+
await notify.transporter.sendMail({
|
|
104
|
+
from:
|
|
105
|
+
SENDER_EMAIL && SENDER_NAME
|
|
106
|
+
? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
|
|
107
|
+
: SMTP_USER,
|
|
108
|
+
to: data.email,
|
|
109
|
+
subject: this.locale('[{{name | safe}}] Registration Confirm Mail', {
|
|
110
|
+
name: SITE_NAME || 'Waline',
|
|
111
|
+
}),
|
|
112
|
+
html: this.locale(
|
|
113
|
+
'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.',
|
|
114
|
+
{ url: apiUrl }
|
|
115
|
+
),
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.log(e);
|
|
119
|
+
|
|
120
|
+
return this.fail(
|
|
121
|
+
this.locale(
|
|
122
|
+
'Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.',
|
|
123
|
+
{ isAdmin: think.isEmpty(count) }
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this.success({ verify: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async putAction() {
|
|
132
|
+
const { display_name, url, avatar, password, type, label } = this.post();
|
|
133
|
+
const { objectId } = this.ctx.state.userInfo;
|
|
134
|
+
const twoFactorAuth = this.post('2fa');
|
|
135
|
+
|
|
136
|
+
const updateData = {};
|
|
137
|
+
|
|
138
|
+
if (this.id && type) {
|
|
139
|
+
updateData.type = type;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (think.isString(label)) {
|
|
143
|
+
updateData.label = label;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (display_name) {
|
|
147
|
+
updateData.display_name = display_name;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (url) {
|
|
151
|
+
updateData.url = url;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (avatar) {
|
|
155
|
+
updateData.avatar = avatar;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (password) {
|
|
159
|
+
updateData.password = new PasswordHash().hashPassword(password);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (think.isString(twoFactorAuth)) {
|
|
163
|
+
updateData['2fa'] = twoFactorAuth;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const socials = ['github', 'twitter', 'facebook', 'google', 'weibo', 'qq'];
|
|
167
|
+
|
|
168
|
+
socials.forEach((social) => {
|
|
169
|
+
const nextSocial = this.post(social);
|
|
170
|
+
|
|
171
|
+
if (think.isString(nextSocial)) {
|
|
172
|
+
updateData[social] = nextSocial;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (think.isEmpty(updateData)) {
|
|
177
|
+
return this.success();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await this.modelInstance.update(updateData, {
|
|
181
|
+
objectId: this.id || objectId,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return this.success();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getUsersListByCount() {
|
|
188
|
+
const { pageSize } = this.get();
|
|
189
|
+
const commentModel = this.service(
|
|
190
|
+
`storage/${this.config('storage')}`,
|
|
191
|
+
'Comment'
|
|
192
|
+
);
|
|
193
|
+
const counts = await commentModel.count(
|
|
194
|
+
{
|
|
195
|
+
status: ['NOT IN', ['waiting', 'spam']],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
group: ['user_id', 'mail'],
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
counts.sort((a, b) => b.count - a.count);
|
|
203
|
+
counts.length = Math.min(pageSize, counts.length);
|
|
204
|
+
|
|
205
|
+
const userIds = counts
|
|
206
|
+
.filter(({ user_id }) => user_id)
|
|
207
|
+
.map(({ user_id }) => user_id);
|
|
208
|
+
|
|
209
|
+
let usersMap = {};
|
|
210
|
+
|
|
211
|
+
if (userIds.length) {
|
|
212
|
+
const users = await this.modelInstance.select({
|
|
213
|
+
objectId: ['IN', userIds],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < users.length; i++) {
|
|
217
|
+
usersMap[users[i].objectId] = users;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const users = [];
|
|
222
|
+
const { avatarProxy } = this.config();
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < counts.length; i++) {
|
|
225
|
+
const count = counts[i];
|
|
226
|
+
const user = {
|
|
227
|
+
count: count.count,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (think.isArray(this.config('levels'))) {
|
|
231
|
+
let level = 0;
|
|
232
|
+
|
|
233
|
+
if (user.count) {
|
|
234
|
+
const _level = think.findLastIndex(
|
|
235
|
+
this.config('levels'),
|
|
236
|
+
(l) => l <= user.count
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (_level !== -1) {
|
|
240
|
+
level = _level;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
user.level = level;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (count.user_id && users[count.user_id]) {
|
|
247
|
+
const {
|
|
248
|
+
display_name: nick,
|
|
249
|
+
url: link,
|
|
250
|
+
avatar: avatarUrl,
|
|
251
|
+
label,
|
|
252
|
+
} = users[count.user_id];
|
|
253
|
+
const avatar =
|
|
254
|
+
avatarProxy && !avatarUrl.includes(avatarProxy)
|
|
255
|
+
? avatarProxy + '?url=' + encodeURIComponent(avatarUrl)
|
|
256
|
+
: avatarUrl;
|
|
257
|
+
|
|
258
|
+
Object.assign(user, { nick, link, avatar, label });
|
|
259
|
+
users.push(user);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const comments = await commentModel.select(
|
|
264
|
+
{ mail: count.mail },
|
|
265
|
+
{ limit: 1 }
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (think.isEmpty(comments)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const comment = comments[0];
|
|
272
|
+
|
|
273
|
+
if (think.isEmpty(comment)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const { nick, link } = comment;
|
|
277
|
+
const avatarUrl = await think.service('avatar').stringify(comment);
|
|
278
|
+
const avatar =
|
|
279
|
+
avatarProxy && !avatarUrl.includes(avatarProxy)
|
|
280
|
+
? avatarProxy + '?url=' + encodeURIComponent(avatarUrl)
|
|
281
|
+
: avatarUrl;
|
|
282
|
+
|
|
283
|
+
Object.assign(user, { nick, link, avatar });
|
|
284
|
+
users.push(user);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return users;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const BaseRest = require('./rest');
|
|
2
|
+
|
|
3
|
+
module.exports = class extends BaseRest {
|
|
4
|
+
constructor(...args) {
|
|
5
|
+
super(...args);
|
|
6
|
+
this.modelInstance = this.service(
|
|
7
|
+
`storage/${this.config('storage')}`,
|
|
8
|
+
'Users'
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async getAction() {
|
|
13
|
+
const { token, email } = this.get();
|
|
14
|
+
const users = await this.modelInstance.select({ email });
|
|
15
|
+
|
|
16
|
+
if (think.isEmpty(users)) {
|
|
17
|
+
return this.fail(this.locale('USER_NOT_EXIST'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const user = users[0];
|
|
21
|
+
const match = user.type.match(/^verify:(\d{4}):(\d+)$/i);
|
|
22
|
+
|
|
23
|
+
if (!match) {
|
|
24
|
+
return this.fail(this.locale('USER_REGISTERED'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (token === match[1] && Date.now() < parseInt(match[2])) {
|
|
28
|
+
await this.modelInstance.update({ type: 'guest' }, { email });
|
|
29
|
+
|
|
30
|
+
return this.redirect('/ui/login');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this.fail(this.locale('TOKEN_EXPIRED'));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const nunjucks = require('nunjucks');
|
|
2
|
+
const locales = require('../locales');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
success(...args) {
|
|
6
|
+
this.ctx.success(...args);
|
|
7
|
+
|
|
8
|
+
return think.prevent();
|
|
9
|
+
},
|
|
10
|
+
fail(...args) {
|
|
11
|
+
this.ctx.fail(...args);
|
|
12
|
+
|
|
13
|
+
return think.prevent();
|
|
14
|
+
},
|
|
15
|
+
locale(message, variables) {
|
|
16
|
+
const { lang } = this.get();
|
|
17
|
+
const locale = locales[(lang || 'zh-cn').toLowerCase()] || locales['zh-cn'];
|
|
18
|
+
|
|
19
|
+
if (locale && locale[message]) {
|
|
20
|
+
message = locale[message];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return nunjucks.renderString(message, variables);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const ip2region = require('dy-node-ip2region');
|
|
2
|
+
const helper = require('think-helper');
|
|
3
|
+
const preventMessage = 'PREVENT_NEXT_PROCESS';
|
|
4
|
+
|
|
5
|
+
const regionSearch = ip2region.create(process.env.IP2REGION_DB);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
prevent() {
|
|
9
|
+
throw new Error(preventMessage);
|
|
10
|
+
},
|
|
11
|
+
isPrevent(err) {
|
|
12
|
+
return think.isError(err) && err.message === preventMessage;
|
|
13
|
+
},
|
|
14
|
+
findLastIndex(arr, fn) {
|
|
15
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
16
|
+
const ret = fn(arr[i], i, arr);
|
|
17
|
+
|
|
18
|
+
if (!ret) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return i;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return -1;
|
|
26
|
+
},
|
|
27
|
+
promiseAllQueue(promises, taskNum) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
if (!promises.length) {
|
|
30
|
+
return resolve();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ret = [];
|
|
34
|
+
let index = 0;
|
|
35
|
+
let count = 0;
|
|
36
|
+
|
|
37
|
+
function runTask() {
|
|
38
|
+
const idx = index;
|
|
39
|
+
|
|
40
|
+
index += 1;
|
|
41
|
+
if (index > promises.length) {
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return promises[idx].then((data) => {
|
|
46
|
+
ret[idx] = data;
|
|
47
|
+
count += 1;
|
|
48
|
+
if (count === promises.length) {
|
|
49
|
+
resolve(ret);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return runTask();
|
|
53
|
+
}, reject);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < taskNum; i++) {
|
|
57
|
+
runTask();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
async ip2region(ip, { depth = 1 }) {
|
|
62
|
+
if (!ip || ip.includes(':')) return '';
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const search = helper.promisify(regionSearch.btreeSearch, regionSearch);
|
|
66
|
+
const result = await search(ip);
|
|
67
|
+
|
|
68
|
+
if (!result) {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
const { region } = result;
|
|
72
|
+
const [, , province, city, isp] = region.split('|');
|
|
73
|
+
const address = Array.from(
|
|
74
|
+
new Set([province, city, isp].filter((v) => v))
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return address.slice(0, depth).join(' ');
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.log(e);
|
|
80
|
+
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "import data format not support!",
|
|
3
|
+
"USER_EXIST": "USER_EXIST",
|
|
4
|
+
"USER_NOT_EXIST": "USER_NOT_EXIST",
|
|
5
|
+
"USER_REGISTERED": "USER_REGISTERED",
|
|
6
|
+
"TOKEN_EXPIRED": "TOKEN_EXPIRED",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "TWO_FACTOR_AUTH_ERROR_DETAIL",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "[{{name | safe}}] Registration Confirm Mail",
|
|
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.": "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.",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "[{{name | safe}}] Reset Password",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!",
|
|
12
|
+
"Duplicate Content": "Duplicate Content",
|
|
13
|
+
"Comment too fast": "Comment too fast",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.",
|
|
15
|
+
"MAIL_SUBJECT": "Your comment on {{site.name | safe}} received a reply",
|
|
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;'> Your comment on <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> received a reply </h2>{{parent.nick}}, you wrote: <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> replied:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p><a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>View full reply</a> or visit <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "New comment on {{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;'> New comment on <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> </h2> <p><strong>{{self.nick}}</strong> wrote:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p><a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>View page</a></p><br/></div>"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "文件格式不支持",
|
|
3
|
+
"USER_EXIST": "用户已存在",
|
|
4
|
+
"USER_NOT_EXIST": "用户不存在",
|
|
5
|
+
"USER_REGISTERED": "用户已注册",
|
|
6
|
+
"TOKEN_EXPIRED": "密钥已过期",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "二步验证失败",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】注册确认邮件",
|
|
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.": "请点击 <a href=\"{{url}}\">{{url}}</a> 确认注册,链接有效时间为 1 个小时。如果不是你在注册,请忽略这封邮件。",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "【{{name | safe}}】重置密码",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "请尽快点击链接 <a href=\"{{url}}\">{{url}}</a> 登录并修改你的密码!",
|
|
12
|
+
"Duplicate Content": "发送的内容之前已经发过",
|
|
13
|
+
"Comment too fast": "评论太快啦,请慢点!",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "注册确认邮件发送失败,请{%- if isAdmin -%}检查一下网站的邮件相关配置{% else %}确认你的邮箱输入无误并联系管理员{%- endif -%}。",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},『{{site.name | safe}}』上的评论收到了回复",
|
|
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;'> 您在<a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a>上的评论有了新的回复 </h2>{{parent.nick}}同学,您曾发表评论: <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>回复说:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>您可以点击<a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>查看回复的完整內容</a>,欢迎再次光临<a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>。</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "{{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;'> 您在<a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a>上的文章有了新的评论 </h2> <p><strong>{{self.nick}}</strong>回复说:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>您可以点击<a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>查看回复的完整內容</a></p><br/> </div>"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "文件格式不支持",
|
|
3
|
+
"USER_EXIST": "用戶已存在",
|
|
4
|
+
"USER_NOT_EXIST": "用戶不存在",
|
|
5
|
+
"USER_REGISTERED": "用戶已註冊",
|
|
6
|
+
"TOKEN_EXPIRED": "密鑰已過期",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "二步驗證失敗",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "『{{name | safe}}』註冊確認郵件",
|
|
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.": "請點擊 <a href=\"{{url}}\">{{url}}</a> 確認註冊,鏈接有效時間為 1 個小時。如果不是你在註冊,請忽略這封郵件。",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "『{{name | safe}}』重置密碼",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "請盡快點擊鏈接 <a href=\"{{url}}\">{{url}}</a> 登錄並修改你的密碼!",
|
|
12
|
+
"Duplicate Content": "發送的內容之前已經發過",
|
|
13
|
+
"Comment too fast": "評論太快啦,請慢點!",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "註冊確認郵件發送失敗,{%- if isAdmin -%}檢查一下網站的郵件相關配置{% else %}確認你的郵箱輸入無誤後聯繫管理員{%- endif -%}。",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},『{{site.name | safe}}』上的評論收到回復",
|
|
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;'> 您在<a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a>上的品論有新的回復 </h2>{{parent.nick}}同學,您層發表評論: <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>回復說:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>您可以點擊<a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>查看回復的完整內容</a>,歡迎再次光臨<a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>。</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "{{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;'> 您在<a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a>上的文章有新評論了 </h2> <p><strong>{{self.nick}}</strong>回復說:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>您可以點擊<a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>查看回復的完整內容</a></p><br/> </div>"
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const Base = require('./base');
|
|
2
|
+
|
|
3
|
+
module.exports = class extends Base {
|
|
4
|
+
getAction() {
|
|
5
|
+
this.rules = {
|
|
6
|
+
path: { array: true },
|
|
7
|
+
type: { array: true, default: ['time'] },
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
postAction() {
|
|
12
|
+
this.rules = {
|
|
13
|
+
path: {
|
|
14
|
+
string: true,
|
|
15
|
+
},
|
|
16
|
+
type: {
|
|
17
|
+
string: true,
|
|
18
|
+
default: 'time',
|
|
19
|
+
},
|
|
20
|
+
action: {
|
|
21
|
+
string: true,
|
|
22
|
+
in: ['inc', 'desc'],
|
|
23
|
+
default: 'inc',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|