@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "Format de fichier non pris en charge",
|
|
3
|
+
"USER_EXIST": "L'utilisateur existe déjà",
|
|
4
|
+
"USER_NOT_EXIST": "L'utilisateur n'existe pas",
|
|
5
|
+
"USER_REGISTERED": "Utilisateur déjà enregistré",
|
|
6
|
+
"TOKEN_EXPIRED": "Le jeton a expiré",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "Échec de l'authentification à deux facteurs",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】Mail de confirmation d'inscription",
|
|
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.": "Veuillez cliquer sur <a href=\"{{url}}\">{{url}}</a> pour confirmer l'inscription. Le lien est valable 1 heure. Si vous ne vous êtes pas inscrit, ignorez cet e-mail.",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "【{{name | safe}}】Réinitialisation du mot de passe",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Veuillez cliquer sur <a href=\"{{url}}\">{{url}}</a> pour vous connecter et changer votre mot de passe dès que possible !",
|
|
12
|
+
"Duplicate Content": "Contenu dupliqué envoyé",
|
|
13
|
+
"Comment too fast": "Vous commentez trop rapidement, veuillez ralentir !",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Échec de l'envoi du mail de confirmation. Veuillez {%- if isAdmin -%}vérifier votre configuration d'e-mail{%- else -%}vérifier votre adresse e-mail et contacter l'administrateur{%- endif -%}.",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},Votre commentaire sur '{{site.name | safe}}' a reçu une réponse",
|
|
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;'> Votre commentaire sur <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> a une nouvelle réponse </h2>{{parent.nick}}, vous avez commenté : <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> a répondu :</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Vous pouvez voir la réponse complète <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>ici</a>, et revenez visiter <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "Nouveau commentaire sur {{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;'> Votre article sur <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> a de nouveaux commentaires </h2> <p><strong>{{self.nick}}</strong> a écrit :</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Vous pouvez voir la réponse complète <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>ici</a></p><br/> </div>"
|
|
19
|
+
}
|
package/src/locales/index.js
CHANGED
|
@@ -2,14 +2,30 @@ const en = require('./en.json');
|
|
|
2
2
|
const it = require('./it.json');
|
|
3
3
|
const zhCN = require('./zh-CN.json');
|
|
4
4
|
const zhTW = require('./zh-TW.json');
|
|
5
|
+
const jp = require('./jp.json');
|
|
6
|
+
const de = require('./de.json');
|
|
7
|
+
const esMX = require('./es.json');
|
|
8
|
+
const fr = require('./fr.json');
|
|
9
|
+
const ru = require('./ru.json');
|
|
10
|
+
const vi = require('./vi-VN.json');
|
|
11
|
+
const ptBR = require('./pt-BR.json');
|
|
5
12
|
|
|
6
13
|
module.exports = {
|
|
7
14
|
'zh-cn': zhCN,
|
|
8
15
|
'zh-tw': zhTW,
|
|
9
16
|
en: en,
|
|
10
17
|
'en-us': en,
|
|
11
|
-
jp: en,
|
|
12
|
-
'jp-jp': en,
|
|
13
18
|
it,
|
|
14
19
|
'it-it': it,
|
|
20
|
+
jp,
|
|
21
|
+
'jp-jp': jp,
|
|
22
|
+
de,
|
|
23
|
+
esMX,
|
|
24
|
+
fr,
|
|
25
|
+
'fr-fr': fr,
|
|
26
|
+
ru,
|
|
27
|
+
'ru-ru': ru,
|
|
28
|
+
vi,
|
|
29
|
+
'vi-vn': vi,
|
|
30
|
+
'pt-br': ptBR,
|
|
15
31
|
};
|
|
@@ -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!": "Formato de arquivo não suportado",
|
|
3
|
+
"USER_EXIST": "Usuário já existe",
|
|
4
|
+
"USER_NOT_EXIST": "Usuário não existe",
|
|
5
|
+
"USER_REGISTERED": "Usuário já registrado",
|
|
6
|
+
"TOKEN_EXPIRED": "Token expirou",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "Erro de autenticação em dois fatores",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】Confirmação 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 clique em <a href=\"{{url}}\">{{url}}</a> para confirmar o registro. O link é válido por 1 hora. Se você não está registrando, ignore este e-mail.",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "【{{name | safe}}】Redefinir senha",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Por favor clique em <a href=\"{{url}}\">{{url}}</a> para entrar e alterar sua senha o quanto antes!",
|
|
12
|
+
"Duplicate Content": "Conteúdo duplicado enviado",
|
|
13
|
+
"Comment too fast": "Comentando rápido demais, por favor aguarde!",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Falha ao enviar o e-mail de confirmação. Por favor {%- if isAdmin -%}verifique a configuração de e-mail{%- else -%}verifique seu endereço de e-mail e contate o administrador{%- endif -%}.",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},Seu comentário em '{{site.name | safe}}' recebeu uma resposta",
|
|
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;'> Seu comentário em <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> tem uma nova resposta </h2>{{parent.nick}}, você comentou: <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> respondeu:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Você pode ver a resposta completa em <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>aqui</a>, e visite novamente <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "Novo comentário em {{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;'> Seu artigo em <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> tem novos comentários </h2> <p><strong>{{self.nick}}</strong> disse:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Você pode ver a resposta completa em <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>aqui</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!": "Định dạng tệp không được hỗ trợ",
|
|
3
|
+
"USER_EXIST": "Người dùng đã tồn tại",
|
|
4
|
+
"USER_NOT_EXIST": "Người dùng không tồn tại",
|
|
5
|
+
"USER_REGISTERED": "Người dùng đã đăng ký",
|
|
6
|
+
"TOKEN_EXPIRED": "Mã thông báo đã hết hạn",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "Xác thực hai bước thất bại",
|
|
8
|
+
"[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】 Thư xác nhận đăng ký",
|
|
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.": "Vui lòng nhấp vào <a href=\"{{url}}\">{{url}}</a> để xác nhận đăng ký. Liên kết có hiệu lực trong 1 giờ. Nếu bạn không đăng ký, hãy bỏ qua email này.",
|
|
10
|
+
"[{{name | safe}}] Reset Password": "【{{name | safe}}】 Đặt lại mật khẩu",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Vui lòng nhấp vào <a href=\"{{url}}\">{{url}}</a> để đăng nhập và thay đổi mật khẩu càng sớm càng tốt!",
|
|
12
|
+
"Duplicate Content": "Nội dung trùng lặp đã được gửi",
|
|
13
|
+
"Comment too fast": "Bình luận quá nhanh, vui lòng chậm lại!",
|
|
14
|
+
"Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Gửi mail xác nhận đăng ký thất bại. Vui lòng {%- if isAdmin -%}kiểm tra cấu hình mail{%- else -%}kiểm tra địa chỉ email và liên hệ quản trị viên{%- endif -%}.",
|
|
15
|
+
"MAIL_SUBJECT": "{{parent.nick | safe}},Bình luận của bạn trên '{{site.name | safe}}' đã có phản hồi",
|
|
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;'> Bình luận của bạn trên <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> đã có phản hồi </h2>{{parent.nick}}, bạn đã bình luận: <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> trả lời:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Bạn có thể xem phản hồi đầy đủ tại <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>đây</a>, và ghé thăm lại <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
|
|
17
|
+
"MAIL_SUBJECT_ADMIN": "Bình luận mới trên {{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;'> Bài viết của bạn trên <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> có bình luận mới </h2> <p><strong>{{self.nick}}</strong> trả lời:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Bạn có thể xem phản hồi đầy đủ tại <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>đây</a></p><br/> </div>"
|
|
19
|
+
}
|
package/src/logic/base.js
CHANGED
|
@@ -3,7 +3,7 @@ const qs = require('node:querystring');
|
|
|
3
3
|
|
|
4
4
|
const jwt = require('jsonwebtoken');
|
|
5
5
|
|
|
6
|
-
module.exports = class extends think.Logic {
|
|
6
|
+
module.exports = class BaseLogic extends think.Logic {
|
|
7
7
|
constructor(...args) {
|
|
8
8
|
super(...args);
|
|
9
9
|
this.modelInstance = this.getModel('Users');
|
|
@@ -11,17 +11,18 @@ module.exports = class extends think.Logic {
|
|
|
11
11
|
this.id = this.getId();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// oxlint-disable-next-line max-statements
|
|
14
15
|
async __before() {
|
|
15
16
|
const referrer = this.ctx.referrer(true);
|
|
16
|
-
let origin = this.ctx
|
|
17
|
+
let { origin } = this.ctx;
|
|
17
18
|
|
|
18
19
|
if (origin) {
|
|
19
20
|
try {
|
|
20
21
|
const parsedOrigin = new URL(origin);
|
|
21
22
|
|
|
22
23
|
origin = parsedOrigin.hostname;
|
|
23
|
-
} catch (
|
|
24
|
-
console.error('Invalid origin format:', origin,
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Invalid origin format:', origin, err);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -39,9 +40,10 @@ module.exports = class extends think.Logic {
|
|
|
39
40
|
// 'api.weibo.com',
|
|
40
41
|
// 'graph.qq.com',
|
|
41
42
|
);
|
|
42
|
-
secureDomains =
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
secureDomains = [
|
|
44
|
+
...secureDomains,
|
|
45
|
+
...this.ctx.state.oauthServices.map(({ origin }) => origin),
|
|
46
|
+
];
|
|
45
47
|
|
|
46
48
|
// 转换可能的正则表达式字符串为正则表达式对象
|
|
47
49
|
secureDomains = secureDomains
|
|
@@ -50,8 +52,8 @@ module.exports = class extends think.Logic {
|
|
|
50
52
|
if (typeof domain === 'string' && domain.startsWith('/') && domain.endsWith('/')) {
|
|
51
53
|
try {
|
|
52
54
|
return new RegExp(domain.slice(1, -1)); // 去掉斜杠并创建 RegExp 对象
|
|
53
|
-
} catch (
|
|
54
|
-
console.error('Invalid regex pattern in secureDomains:', domain,
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Invalid regex pattern in secureDomains:', domain, err);
|
|
55
57
|
|
|
56
58
|
return null;
|
|
57
59
|
}
|
|
@@ -62,7 +64,7 @@ module.exports = class extends think.Logic {
|
|
|
62
64
|
.filter(Boolean); // 过滤掉无效的正则表达式
|
|
63
65
|
|
|
64
66
|
// 有 referrer 检查 referrer,没有则检查 origin
|
|
65
|
-
const checking = referrer
|
|
67
|
+
const checking = referrer || origin;
|
|
66
68
|
const isSafe = secureDomains.some((domain) =>
|
|
67
69
|
think.isFunction(domain.test) ? domain.test(checking) : domain === checking,
|
|
68
70
|
);
|
|
@@ -84,8 +86,8 @@ module.exports = class extends think.Logic {
|
|
|
84
86
|
|
|
85
87
|
try {
|
|
86
88
|
userId = jwt.verify(token, think.config('jwtKey'));
|
|
87
|
-
} catch (
|
|
88
|
-
think.logger.debug(
|
|
89
|
+
} catch (err) {
|
|
90
|
+
think.logger.debug(err);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
if (think.isEmpty(userId) || !think.isString(userId)) {
|
|
@@ -93,7 +95,7 @@ module.exports = class extends think.Logic {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
const user = await this.modelInstance.select(
|
|
96
|
-
{ objectId: userId },
|
|
98
|
+
{ objectId: userId, type: ['!=', 'banned'] },
|
|
97
99
|
{
|
|
98
100
|
field: [
|
|
99
101
|
'id',
|
|
@@ -113,19 +115,19 @@ module.exports = class extends think.Logic {
|
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
const userInfo = user
|
|
118
|
+
const [userInfo] = user;
|
|
117
119
|
|
|
118
|
-
let avatarUrl =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
let avatarUrl =
|
|
121
|
+
userInfo.avatar ||
|
|
122
|
+
(await think.service('avatar').stringify({
|
|
123
|
+
mail: userInfo.email,
|
|
124
|
+
nick: userInfo.display_name,
|
|
125
|
+
link: userInfo.url,
|
|
126
|
+
}));
|
|
125
127
|
const { avatarProxy } = think.config();
|
|
126
128
|
|
|
127
129
|
if (avatarProxy) {
|
|
128
|
-
avatarUrl = avatarProxy
|
|
130
|
+
avatarUrl = `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`;
|
|
129
131
|
}
|
|
130
132
|
userInfo.avatar = avatarUrl;
|
|
131
133
|
this.ctx.state.userInfo = userInfo;
|
|
@@ -136,7 +138,7 @@ module.exports = class extends think.Logic {
|
|
|
136
138
|
const filename = this.__filename || __filename;
|
|
137
139
|
const last = filename.lastIndexOf(path.sep);
|
|
138
140
|
|
|
139
|
-
return filename.
|
|
141
|
+
return filename.slice(last + 1, filename.length - last - 4);
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
getId() {
|
|
@@ -193,7 +195,7 @@ module.exports = class extends think.Logic {
|
|
|
193
195
|
remoteip: this.ctx.ip,
|
|
194
196
|
});
|
|
195
197
|
|
|
196
|
-
const requestUrl = method === 'GET' ? api
|
|
198
|
+
const requestUrl = method === 'GET' ? `${api}?${query}` : api;
|
|
197
199
|
const options =
|
|
198
200
|
method === 'GET'
|
|
199
201
|
? {}
|
package/src/logic/comment.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const Base = require('./base.js');
|
|
2
2
|
|
|
3
|
-
module.exports = class extends Base {
|
|
3
|
+
module.exports = class CommentLogic extends Base {
|
|
4
4
|
checkAdmin() {
|
|
5
5
|
const { userInfo } = this.ctx.state;
|
|
6
6
|
|
|
@@ -117,7 +117,7 @@ module.exports = class extends Base {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
switch (type) {
|
|
120
|
-
case 'recent':
|
|
120
|
+
case 'recent': {
|
|
121
121
|
this.rules = {
|
|
122
122
|
count: {
|
|
123
123
|
int: { max: 50 },
|
|
@@ -125,14 +125,16 @@ module.exports = class extends Base {
|
|
|
125
125
|
},
|
|
126
126
|
};
|
|
127
127
|
break;
|
|
128
|
+
}
|
|
128
129
|
|
|
129
|
-
case 'count':
|
|
130
|
+
case 'count': {
|
|
130
131
|
this.rules = {
|
|
131
132
|
url: {
|
|
132
133
|
array: true,
|
|
133
134
|
},
|
|
134
135
|
};
|
|
135
136
|
break;
|
|
137
|
+
}
|
|
136
138
|
|
|
137
139
|
case 'list': {
|
|
138
140
|
const { userInfo } = this.ctx.state;
|
|
@@ -153,7 +155,7 @@ module.exports = class extends Base {
|
|
|
153
155
|
break;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
default:
|
|
158
|
+
default: {
|
|
157
159
|
this.rules = {
|
|
158
160
|
path: {
|
|
159
161
|
string: true,
|
|
@@ -173,6 +175,7 @@ module.exports = class extends Base {
|
|
|
173
175
|
},
|
|
174
176
|
};
|
|
175
177
|
break;
|
|
178
|
+
}
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
|
package/src/logic/db.js
CHANGED
package/src/logic/token.js
CHANGED
|
@@ -34,8 +34,8 @@ module.exports = class extends Base {
|
|
|
34
34
|
* @apiSuccess (200) {Number} errno 0
|
|
35
35
|
* @apiSuccess (200) {String} errmsg return error message if error
|
|
36
36
|
*/
|
|
37
|
-
postAction() {
|
|
38
|
-
|
|
37
|
+
async postAction() {
|
|
38
|
+
await this.useCaptchaCheck();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
package/src/logic/user.js
CHANGED
|
@@ -121,4 +121,29 @@ module.exports = class extends Base {
|
|
|
121
121
|
},
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @api {DELETE} /api/user delete user account when user is in verify status, or forbid user to login when user is other status.
|
|
127
|
+
* @apiGroup User
|
|
128
|
+
* @apiVersion 0.0.1
|
|
129
|
+
*
|
|
130
|
+
* @apiParam {String} id user id
|
|
131
|
+
* @apiParam {String} lang language
|
|
132
|
+
*
|
|
133
|
+
* @apiSuccess (200) {Number} errno 0
|
|
134
|
+
* @apiSuccess (200) {String} errmsg return error message if error
|
|
135
|
+
*/
|
|
136
|
+
deleteAction() {
|
|
137
|
+
// you need to be logged in to delete users
|
|
138
|
+
const { userInfo } = this.ctx.state;
|
|
139
|
+
|
|
140
|
+
if (think.isEmpty(userInfo)) {
|
|
141
|
+
return this.fail();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// you must be an administrator to delete other users and cannot delete yourself
|
|
145
|
+
if (userInfo.type !== 'administrator' || this.id === userInfo.objectId) {
|
|
146
|
+
return this.fail();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
124
149
|
};
|
package/src/middleware/plugin.js
CHANGED
|
@@ -3,7 +3,7 @@ const compose = require('koa-compose');
|
|
|
3
3
|
module.exports = () => async (ctx, next) => {
|
|
4
4
|
const middlewares = think.getPluginMiddlewares();
|
|
5
5
|
|
|
6
|
-
if (!think.isArray(middlewares) ||
|
|
6
|
+
if (!think.isArray(middlewares) || middlewares.length === 0) {
|
|
7
7
|
return next();
|
|
8
8
|
}
|
|
9
9
|
|
package/src/service/akismet.js
CHANGED
|
@@ -2,6 +2,7 @@ const Akismet = require('akismet');
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_KEY = '70542d86693e';
|
|
4
4
|
|
|
5
|
+
// oxlint-disable-next-line func-names
|
|
5
6
|
module.exports = function (comment, blog) {
|
|
6
7
|
let { AKISMET_KEY, SITE_URL } = process.env;
|
|
7
8
|
|
|
@@ -13,14 +14,18 @@ module.exports = function (comment, blog) {
|
|
|
13
14
|
return Promise.resolve(false);
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
// oxlint-disable-next-line func-names
|
|
16
18
|
return new Promise(function (resolve, reject) {
|
|
17
19
|
const akismet = Akismet.client({ blog, apiKey: AKISMET_KEY });
|
|
18
20
|
|
|
21
|
+
// oxlint-disable-next-line func-names
|
|
19
22
|
akismet.verifyKey(function (err, verifyKey) {
|
|
20
23
|
if (err) {
|
|
21
|
-
|
|
24
|
+
reject(err);
|
|
25
|
+
return;
|
|
22
26
|
} else if (!verifyKey) {
|
|
23
|
-
|
|
27
|
+
reject(new Error('Akismet API_KEY verify failed!'));
|
|
28
|
+
return;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
akismet.checkComment(
|
|
@@ -30,9 +35,11 @@ module.exports = function (comment, blog) {
|
|
|
30
35
|
comment_author: comment.nick,
|
|
31
36
|
comment_content: comment.comment,
|
|
32
37
|
},
|
|
38
|
+
// oxlint-disable-next-line func-names
|
|
33
39
|
function (err, spam) {
|
|
34
40
|
if (err) {
|
|
35
|
-
|
|
41
|
+
reject(err);
|
|
42
|
+
return;
|
|
36
43
|
}
|
|
37
44
|
resolve(spam);
|
|
38
45
|
},
|
package/src/service/avatar.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const escapeHtml = (unsafeHTML) =>
|
|
2
2
|
unsafeHTML
|
|
3
|
-
.
|
|
4
|
-
.
|
|
5
|
-
.
|
|
6
|
-
.
|
|
7
|
-
.
|
|
3
|
+
.replaceAll('&', '&')
|
|
4
|
+
.replaceAll('<', '<')
|
|
5
|
+
.replaceAll('>', '>')
|
|
6
|
+
.replaceAll('"', '"')
|
|
7
|
+
.replaceAll("'", ''');
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
escapeHtml,
|
|
@@ -28,16 +28,11 @@ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const sanitize = (content) =>
|
|
31
|
-
DOMPurify.sanitize(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
FORBID_ATTR: ['autoplay', 'style'],
|
|
37
|
-
},
|
|
38
|
-
think.config('domPurify') || {},
|
|
39
|
-
),
|
|
40
|
-
);
|
|
31
|
+
DOMPurify.sanitize(content, {
|
|
32
|
+
FORBID_TAGS: ['form', 'input', 'style'],
|
|
33
|
+
FORBID_ATTR: ['autoplay', 'style'],
|
|
34
|
+
...think.config('domPurify'),
|
|
35
|
+
});
|
|
41
36
|
|
|
42
37
|
module.exports = {
|
|
43
38
|
sanitize,
|